From be46a81b8ff702f9820ba3374573f40ec6eb133d Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 1 Nov 2024 19:15:12 +0100 Subject: [PATCH 001/324] Refactor - Breaking Changes for current DockStat frontend (#18) * Advanced logging * remove docker.io/library from images * updated entrypoint for cup integration * added swagger and more * updated new routes and added first routes which will be controlled by the frontend * Added auth (still buggy here and there) * fixed auth problem when trying to use swagger * Add more logging * added databse functionality * test for auto commit message * test auto git changelog * Update changelog.yml * Update changelog.yml * test new CHANGELOG.md * Changed Files: -------------- .github/workflows/changelog.yml CHANGELOG.md data/database.db package-lock.json package.json * Added offline development capabilities * Advanced frontend customization and new dependecy graph generator using mermaid diagrams and dependecy-cruiser * Dependency cruiser and gitignore adjustmnts * better port assignment, when running in a non-docker environment * Adjust workflows and adjusted entrypoint.sh file * Rate limiter * Rate limiter * adjust workflows --------- Co-authored-by: ItsNik Co-authored-by: root --- .dependency-cruiser.js | 380 +++ .github/workflows/build-dev.yaml | 43 +- .github/workflows/build-image.yml | 44 +- .gitignore | 147 +- Dockerfile | 66 +- LICENSE | 56 +- README.md | 87 +- config/apprise_config_example.yml | 5 - config/db.js | 19 + config/dockerConfig.json | 9 + config/hosts.yaml | 24 - config/loggerConfig.js | 16 + config/swaggerConfig.js | 28 + controllers/containerController.js | 49 + controllers/fetchData.js | 34 + controllers/frontendConfiguration.js | 180 ++ controllers/scheduler.js | 75 + data/database.db | Bin 0 -> 16384 bytes dockstatapi.js | 379 --- entrypoint.sh | 27 +- logger.js | 24 - middleware/authMiddleware.js | 50 + middleware/password.json | 1 + middleware/usePassword.txt | 1 + misc/dependencyGraphs/mermaid-all.txt | 70 + misc/dependencyGraphs/mermaid-api.txt | 33 + misc/dependencyGraphs/mermaid-auth.txt | 8 + misc/dependencyGraphs/mermaid-conf.txt | 26 + misc/dependencyGraphs/mermaid-data.txt | 11 + misc/dependencyGraphs/mermaid-frontend.txt | 11 + misc/entrypoint.sh | 26 + modules/updateAvailable.js | 32 - package-lock.json | 2507 +++++++++++++++++++- package.json | 24 +- routes/auth/routes.js | 145 ++ routes/data/routes.js | 111 + routes/frontendController/routes.js | 340 +++ routes/getter/routes.js | 334 +++ routes/setter/routes.js | 145 ++ scripts/install_apprise.sh | 16 - scripts/notify.sh | 47 - server.js | 33 + swagger/swaggerDocs.js | 10 + utils/containerService.js | 63 + utils/createDependencyGraph.sh | 34 + utils/dockerClient.js | 45 + utils/extractHostData.js | 26 + utils/logger.js | 20 + utils/rateLimiter.js | 8 + utils/writeOfflineLog.js | 31 + 50 files changed, 5150 insertions(+), 750 deletions(-) create mode 100644 .dependency-cruiser.js delete mode 100644 config/apprise_config_example.yml create mode 100644 config/db.js create mode 100644 config/dockerConfig.json delete mode 100644 config/hosts.yaml create mode 100644 config/loggerConfig.js create mode 100644 config/swaggerConfig.js create mode 100644 controllers/containerController.js create mode 100644 controllers/fetchData.js create mode 100644 controllers/frontendConfiguration.js create mode 100644 controllers/scheduler.js create mode 100644 data/database.db delete mode 100644 dockstatapi.js delete mode 100644 logger.js create mode 100644 middleware/authMiddleware.js create mode 100644 middleware/password.json create mode 100644 middleware/usePassword.txt create mode 100644 misc/dependencyGraphs/mermaid-all.txt create mode 100644 misc/dependencyGraphs/mermaid-api.txt create mode 100644 misc/dependencyGraphs/mermaid-auth.txt create mode 100644 misc/dependencyGraphs/mermaid-conf.txt create mode 100644 misc/dependencyGraphs/mermaid-data.txt create mode 100644 misc/dependencyGraphs/mermaid-frontend.txt create mode 100644 misc/entrypoint.sh delete mode 100644 modules/updateAvailable.js create mode 100644 routes/auth/routes.js create mode 100644 routes/data/routes.js create mode 100644 routes/frontendController/routes.js create mode 100644 routes/getter/routes.js create mode 100644 routes/setter/routes.js delete mode 100644 scripts/install_apprise.sh delete mode 100755 scripts/notify.sh create mode 100644 server.js create mode 100644 swagger/swaggerDocs.js create mode 100644 utils/containerService.js create mode 100755 utils/createDependencyGraph.sh create mode 100644 utils/dockerClient.js create mode 100644 utils/extractHostData.js create mode 100644 utils/logger.js create mode 100644 utils/rateLimiter.js create mode 100644 utils/writeOfflineLog.js diff --git a/.dependency-cruiser.js b/.dependency-cruiser.js new file mode 100644 index 0000000..07df12b --- /dev/null +++ b/.dependency-cruiser.js @@ -0,0 +1,380 @@ +/** @type {import('dependency-cruiser').IConfiguration} */ +module.exports = { + forbidden: [ + { + name: 'no-circular', + severity: 'warn', + comment: + 'This dependency is part of a circular relationship. You might want to revise ' + + 'your solution (i.e. use dependency inversion, make sure the modules have a single responsibility) ', + from: {}, + to: { + circular: true + } + }, + { + name: 'no-orphans', + comment: + "This is an orphan module - it's likely not used (anymore?). Either use it or " + + "remove it. If it's logical this module is an orphan (i.e. it's a config file), " + + "add an exception for it in your dependency-cruiser configuration. By default " + + "this rule does not scrutinize dot-files (e.g. .eslintrc.js), TypeScript declaration " + + "files (.d.ts), tsconfig.json and some of the babel and webpack configs.", + severity: 'warn', + from: { + orphan: true, + pathNot: [ + '(^|/)[.][^/]+[.](?:js|cjs|mjs|ts|cts|mts|json)$', // dot files + '[.]d[.]ts$', // TypeScript declaration files + '(^|/)tsconfig[.]json$', // TypeScript config + '(^|/)(?:babel|webpack)[.]config[.](?:js|cjs|mjs|ts|cts|mts|json)$' // other configs + ] + }, + to: {}, + }, + { + name: 'no-deprecated-core', + comment: + 'A module depends on a node core module that has been deprecated. Find an alternative - these are ' + + "bound to exist - node doesn't deprecate lightly.", + severity: 'warn', + from: {}, + to: { + dependencyTypes: [ + 'core' + ], + path: [ + '^v8/tools/codemap$', + '^v8/tools/consarray$', + '^v8/tools/csvparser$', + '^v8/tools/logreader$', + '^v8/tools/profile_view$', + '^v8/tools/profile$', + '^v8/tools/SourceMap$', + '^v8/tools/splaytree$', + '^v8/tools/tickprocessor-driver$', + '^v8/tools/tickprocessor$', + '^node-inspect/lib/_inspect$', + '^node-inspect/lib/internal/inspect_client$', + '^node-inspect/lib/internal/inspect_repl$', + '^async_hooks$', + '^punycode$', + '^domain$', + '^constants$', + '^sys$', + '^_linklist$', + '^_stream_wrap$' + ], + } + }, + { + name: 'not-to-deprecated', + comment: + 'This module uses a (version of an) npm module that has been deprecated. Either upgrade to a later ' + + 'version of that module, or find an alternative. Deprecated modules are a security risk.', + severity: 'warn', + from: {}, + to: { + dependencyTypes: [ + 'deprecated' + ] + } + }, + { + name: 'no-non-package-json', + severity: 'error', + comment: + "This module depends on an npm package that isn't in the 'dependencies' section of your package.json. " + + "That's problematic as the package either (1) won't be available on live (2 - worse) will be " + + "available on live with an non-guaranteed version. Fix it by adding the package to the dependencies " + + "in your package.json.", + from: {}, + to: { + dependencyTypes: [ + 'npm-no-pkg', + 'npm-unknown' + ] + } + }, + { + name: 'not-to-unresolvable', + comment: + "This module depends on a module that cannot be found ('resolved to disk'). If it's an npm " + + 'module: add it to your package.json. In all other cases you likely already know what to do.', + severity: 'error', + from: {}, + to: { + couldNotResolve: true + } + }, + { + name: 'no-duplicate-dep-types', + comment: + "Likely this module depends on an external ('npm') package that occurs more than once " + + "in your package.json i.e. bot as a devDependencies and in dependencies. This will cause " + + "maintenance problems later on.", + severity: 'warn', + from: {}, + to: { + moreThanOneDependencyType: true, + // as it's pretty common to have a type import be a type only import + // _and_ (e.g.) a devDependency - don't consider type-only dependency + // types for this rule + dependencyTypesNot: ["type-only"] + } + }, + + /* rules you might want to tweak for your specific situation: */ + + { + name: 'not-to-spec', + comment: + 'This module depends on a spec (test) file. The sole responsibility of a spec file is to test code. ' + + "If there's something in a spec that's of use to other modules, it doesn't have that single " + + 'responsibility anymore. Factor it out into (e.g.) a separate utility/ helper or a mock.', + severity: 'error', + from: {}, + to: { + path: '[.](?:spec|test)[.](?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$' + } + }, + { + name: 'not-to-dev-dep', + severity: 'error', + comment: + "This module depends on an npm package from the 'devDependencies' section of your " + + 'package.json. It looks like something that ships to production, though. To prevent problems ' + + "with npm packages that aren't there on production declare it (only!) in the 'dependencies'" + + 'section of your package.json. If this module is development only - add it to the ' + + 'from.pathNot re of the not-to-dev-dep rule in the dependency-cruiser configuration', + from: { + path: '^(\./)', + pathNot: '[.](?:spec|test)[.](?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$' + }, + to: { + dependencyTypes: [ + 'npm-dev', + ], + // type only dependencies are not a problem as they don't end up in the + // production code or are ignored by the runtime. + dependencyTypesNot: [ + 'type-only' + ], + pathNot: [ + 'node_modules/@types/' + ] + } + }, + { + name: 'optional-deps-used', + severity: 'info', + comment: + "This module depends on an npm package that is declared as an optional dependency " + + "in your package.json. As this makes sense in limited situations only, it's flagged here. " + + "If you're using an optional dependency here by design - add an exception to your" + + "dependency-cruiser configuration.", + from: {}, + to: { + dependencyTypes: [ + 'npm-optional' + ] + } + }, + { + name: 'peer-deps-used', + comment: + "This module depends on an npm package that is declared as a peer dependency " + + "in your package.json. This makes sense if your package is e.g. a plugin, but in " + + "other cases - maybe not so much. If the use of a peer dependency is intentional " + + "add an exception to your dependency-cruiser configuration.", + severity: 'warn', + from: {}, + to: { + dependencyTypes: [ + 'npm-peer' + ] + } + } + ], + options: { + + /* Which modules not to follow further when encountered */ + doNotFollow: { + /* path: an array of regular expressions in strings to match against */ + path: ['node_modules'] + }, + + /* Which modules to exclude */ + // exclude : { + // /* path: an array of regular expressions in strings to match against */ + // path: '', + // }, + + /* Which modules to exclusively include (array of regular expressions in strings) + dependency-cruiser will skip everything not matching this pattern + */ + // includeOnly : [''], + + /* List of module systems to cruise. + When left out dependency-cruiser will fall back to the list of _all_ + module systems it knows of. It's the default because it's the safe option + It might come at a performance penalty, though. + moduleSystems: ['amd', 'cjs', 'es6', 'tsd'] + + As in practice only commonjs ('cjs') and ecmascript modules ('es6') + are widely used, you can limit the moduleSystems to those. + */ + + // moduleSystems: ['cjs', 'es6'], + + /* prefix for links in html and svg output (e.g. 'https://github.com/you/yourrepo/blob/main/' + to open it on your online repo or `vscode://file/${process.cwd()}/` to + open it in visual studio code), + */ + // prefix: `vscode://file/${process.cwd()}/`, + + /* false (the default): ignore dependencies that only exist before typescript-to-javascript compilation + true: also detect dependencies that only exist before typescript-to-javascript compilation + "specify": for each dependency identify whether it only exists before compilation or also after + */ + // tsPreCompilationDeps: false, + + /* list of extensions to scan that aren't javascript or compile-to-javascript. + Empty by default. Only put extensions in here that you want to take into + account that are _not_ parsable. + */ + // extraExtensionsToScan: [".json", ".jpg", ".png", ".svg", ".webp"], + + /* if true combines the package.jsons found from the module up to the base + folder the cruise is initiated from. Useful for how (some) mono-repos + manage dependencies & dependency definitions. + */ + // combinedDependencies: false, + + /* if true leave symlinks untouched, otherwise use the realpath */ + // preserveSymlinks: false, + + /* TypeScript project file ('tsconfig.json') to use for + (1) compilation and + (2) resolution (e.g. with the paths property) + + The (optional) fileName attribute specifies which file to take (relative to + dependency-cruiser's current working directory). When not provided + defaults to './tsconfig.json'. + */ + // tsConfig: { + // fileName: 'tsconfig.json' + // }, + + /* Webpack configuration to use to get resolve options from. + + The (optional) fileName attribute specifies which file to take (relative + to dependency-cruiser's current working directory. When not provided defaults + to './webpack.conf.js'. + + The (optional) `env` and `arguments` attributes contain the parameters + to be passed if your webpack config is a function and takes them (see + webpack documentation for details) + */ + // webpackConfig: { + // fileName: 'webpack.config.js', + // env: {}, + // arguments: {} + // }, + + /* Babel config ('.babelrc', '.babelrc.json', '.babelrc.json5', ...) to use + for compilation + */ + // babelConfig: { + // fileName: '.babelrc', + // }, + + /* List of strings you have in use in addition to cjs/ es6 requires + & imports to declare module dependencies. Use this e.g. if you've + re-declared require, use a require-wrapper or use window.require as + a hack. + */ + // exoticRequireStrings: [], + + /* options to pass on to enhanced-resolve, the package dependency-cruiser + uses to resolve module references to disk. The values below should be + suitable for most situations + + If you use webpack: you can also set these in webpack.conf.js. The set + there will override the ones specified here. + */ + enhancedResolveOptions: { + /* What to consider as an 'exports' field in package.jsons */ + exportsFields: ["exports"], + /* List of conditions to check for in the exports field. + Only works when the 'exportsFields' array is non-empty. + */ + conditionNames: ["import", "require", "node", "default", "types"], + /* + The extensions, by default are the same as the ones dependency-cruiser + can access (run `npx depcruise --info` to see which ones that are in + _your_ environment). If that list is larger than you need you can pass + the extensions you actually use (e.g. [".js", ".jsx"]). This can speed + up module resolution, which is the most expensive step. + */ + // extensions: [".js", ".jsx", ".ts", ".tsx", ".d.ts"], + /* What to consider a 'main' field in package.json */ + + // if you migrate to ESM (or are in an ESM environment already) you will want to + // have "module" in the list of mainFields, like so: + // mainFields: ["module", "main", "types", "typings"], + mainFields: ["main", "types", "typings"], + /* + A list of alias fields in package.jsons + See [this specification](https://github.com/defunctzombie/package-browser-field-spec) and + the webpack [resolve.alias](https://webpack.js.org/configuration/resolve/#resolvealiasfields) + documentation + + Defaults to an empty array (= don't use alias fields). + */ + // aliasFields: ["browser"], + }, + reporterOptions: { + dot: { + /* pattern of modules that can be consolidated in the detailed + graphical dependency graph. The default pattern in this configuration + collapses everything in node_modules to one folder deep so you see + the external modules, but their innards. + */ + collapsePattern: 'node_modules/(?:@[^/]+/[^/]+|[^/]+)', + + /* Options to tweak the appearance of your graph.See + https://github.com/sverweij/dependency-cruiser/blob/main/doc/options-reference.md#reporteroptions + for details and some examples. If you don't specify a theme + dependency-cruiser falls back to a built-in one. + */ + // theme: { + // graph: { + // /* splines: "ortho" gives straight lines, but is slow on big graphs + // splines: "true" gives bezier curves (fast, not as nice as ortho) + // */ + // splines: "true" + // }, + // } + }, + archi: { + /* pattern of modules that can be consolidated in the high level + graphical dependency graph. If you use the high level graphical + dependency graph reporter (`archi`) you probably want to tweak + this collapsePattern to your situation. + */ + collapsePattern: '^(?:packages|src|lib(s?)|app(s?)|bin|test(s?)|spec(s?))/[^/]+|node_modules/(?:@[^/]+/[^/]+|[^/]+)', + + /* Options to tweak the appearance of your graph. If you don't specify a + theme for 'archi' dependency-cruiser will use the one specified in the + dot section above and otherwise use the default one. + */ + // theme: { }, + }, + "text": { + "highlightFocused": true + }, + } + } +}; +// generated: dependency-cruiser@16.5.0 on 2024-10-31T20:09:59.974Z diff --git a/.github/workflows/build-dev.yaml b/.github/workflows/build-dev.yaml index 72a370e..a8d55f2 100644 --- a/.github/workflows/build-dev.yaml +++ b/.github/workflows/build-dev.yaml @@ -2,7 +2,8 @@ name: Docker Image CI (dev) on: push: - branches: [ "dev" ] + branches: + - "dev" permissions: packages: write @@ -12,15 +13,35 @@ jobs: build-main: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - name: Checkout repository + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 - - uses: pmorelli92/github-container-registry-build-push@2.2.1 - name: Build and Publish latest service image + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Github Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ github.token }} + + - name: Generate Docker tags + uses: docker/metadata-action@v5 + id: metadata + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=sha,format=long,prefix= + flavor: | + type=schedule,pattern=nightly + + - name: Build and push + uses: docker/build-push-action@v5 with: - github-push-secret: ${{secrets.GITHUB_TOKEN}} - docker-image-name: dockstatapi - docker-image-tag: dev # optional - dockerfile-path: Dockerfile # optional - build-context: . # optional - build-only: false # optional + platforms: linux/amd64,linux/arm64,linux/arm/v7 + push: true + tags: ${{ steps.metadata.outputs.tags }} + labels: ${{ steps.metadata.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index 2836ac9..8668f9b 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -1,8 +1,8 @@ name: Docker Image CI on: - push: - branches: [ "main" ] + release: + types: [published] permissions: packages: write @@ -12,15 +12,35 @@ jobs: build-main: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - name: Checkout repository + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 - - uses: pmorelli92/github-container-registry-build-push@2.2.1 - name: Build and Publish latest service image + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Github Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ github.token }} + + - name: Generate Docker tags + uses: docker/metadata-action@v5 + id: metadata + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=sha,format=long,prefix= + flavor: | + latest=true + + - name: Build and push + uses: docker/build-push-action@v5 with: - github-push-secret: ${{secrets.GITHUB_TOKEN}} - docker-image-name: dockstatapi - docker-image-tag: latest # optional - dockerfile-path: Dockerfile # optional - build-context: . # optional - build-only: false # optional + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.metadata.outputs.tags }} + labels: ${{ steps.metadata.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.gitignore b/.gitignore index 2e7f14a..f7fcc52 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,143 @@ -dockstat.log -node_modules -.dockerignore -apprise_config.yml \ No newline at end of file +# custom paths: +data/* + +# Created by https://www.toptal.com/developers/gitignore/api/node +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### Node Patch ### +# Serverless Webpack directories +.webpack/ + +# Optional stylelint cache + +# SvelteKit build / generate output +.svelte-kit diff --git a/Dockerfile b/Dockerfile index 8c70ae6..5fc294e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,34 +1,32 @@ -# Stage 1: Build stage -FROM node:latest AS builder - -LABEL maintainer="https://github.com/its4nik" -LABEL version="1.0" -LABEL description="API for DockStat: Docker container statistics." -LABEL license="MIT" -LABEL repository="https://github.com/its4nik/dockstatapi" -LABEL documentation="https://github.com/its4nik/dockstatapi" - -WORKDIR /api - -COPY package*.json ./ - -RUN npm install --production - -COPY . . - -# Stage 2: Production stage -FROM node:alpine - -WORKDIR /api - -COPY --from=builder /api . - -RUN apk add --no-cache bash curl - -RUN bash /api/scripts/install_apprise.sh - -EXPOSE 7070 - -HEALTHCHECK CMD curl --fail http://localhost:7070/ || exit 1 - -ENTRYPOINT [ "bash", "entrypoint.sh" ] +# Stage 1: Build stage +FROM node:latest AS builder + +LABEL maintainer="https://github.com/its4nik" +LABEL version="2" +LABEL description="API for DockStat" +LABEL license="BSD-3-Clause license " +LABEL repository="https://github.com/its4nik/dockstatapi" +LABEL documentation="https://github.com/its4nik/dockstatapi" + +WORKDIR /api + +COPY package*.json ./ + +RUN npm install --production + +COPY . . + +# Stage 2: Production stage +FROM node:alpine + +WORKDIR /api + +COPY --from=builder /api . + +RUN apk add --no-cache bash curl + +EXPOSE 7070 + +HEALTHCHECK CMD curl --fail http://localhost:7070/api/status || exit 1 + +ENTRYPOINT [ "bash", "misc/entrypoint.sh" ] diff --git a/LICENSE b/LICENSE index 0a73124..1e9eceb 100644 --- a/LICENSE +++ b/LICENSE @@ -1,28 +1,28 @@ -BSD 3-Clause License - -Copyright (c) 2024, ItsNik - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +BSD 3-Clause License + +Copyright (c) 2024, ItsNik + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index 0735e1a..c12afae 100644 --- a/README.md +++ b/README.md @@ -1,82 +1,13 @@ -# DockstatAPI +# DockStatAPI v2 -## This tool relies on the [DockerSocket Proxy](https://docs.linuxserver.io/images/docker-socket-proxy/), please see it's documentation for more information. +This specific branch contains the currently WIP **DockStatAPI-v2**, this update will bring major breaking changes so please be careful. +With this new release a cupple of extra features (compared to v1) are going to be available. -This is the DockStatAPI used in [DockStat](https://github.com/its4nik/dockstat). +### Feature List: -It features an easy way to configure using a yaml file. +- Swagger API Documentation +- "Offline" mode (useful when working on the backend without available test docker sockets) +- Database (Keeps data for 24 hours max) +- Advanced authentication using hashes and salt -You can specify multiple hosts, using a Docker Socket Proxy like this: - -## Installation: - -docker-compose.yaml -```yaml -services: - dockstatapi: - image: ghcr.io/its4nik/dockstatapi:latest - container_name: dockstatapi - environment: - - SECRET=CHANGEME # This is required in the header 'Authorization': 'CHANGEME' - - ALLOW_LOCALHOST="False" # Defaults to False - ports: - - "7070:7070" - volumes: - - ./dockstatapi:/api/config # Place your hosts.yaml file here - restart: always -``` - -Example docker-socket onfiguration: - -```yaml -socket-proxy: - image: lscr.io/linuxserver/socket-proxy:latest - container_name: socket-proxy - environment: - - CONTAINERS=1 # Needed for the api to worrk - - INFO=1 # Needed for the api to work - volumes: - - /var/run/docker.sock:/var/run/docker.sock:ro - restart: unless-stopped - read_only: true - tmpfs: - - /run - ports: - - 2375:2375 -``` - -Configuration inside the mounted folder, as hosts.yaml -```yaml -mintimeout: 10000 # The minimum time to wait before querying the same server again, defaults to 5000 Ms - -log: - logsize: 10 # Specify the Size of the log files in MB, default is 1MB - LogCount: 1 # How many log files should be kept in rotation. Default is 5 - -hosts: - YourHost1: - url: hetzner - port: 2375 - -# This is used for DockStat -# Please see the dockstat documentation for more information -tags: - raspberry: red-200 - private: violet-400 - -container: - dozzle: # Container name - link: https://github.com -``` - -Please see the documentation for more information on what endpoints will be provieded. - -[Documentation](https://outline.itsnik.de/s/dockstat/doc/backend-api-reference-YzcBbDvY33) - ---- - -This Api uses a "queuing" mechanism to communicate to the servers, so that we dont ask the same server multiple times without getting an answer. - -Feel free to use this API in any of your projects :D - -The `/stats` endpoint server all information that are gethered from the server in a json format. +# 🔗 DockStatAPI v2 Documentation diff --git a/config/apprise_config_example.yml b/config/apprise_config_example.yml deleted file mode 100644 index 88e3387..0000000 --- a/config/apprise_config_example.yml +++ /dev/null @@ -1,5 +0,0 @@ -# Please see the apprise documentation -urls: - - tgram://bottoken/ChatID - - rocket://user:password@hostname/RoomID/Channel - - ntfy://topic/ diff --git a/config/db.js b/config/db.js new file mode 100644 index 0000000..9317ab4 --- /dev/null +++ b/config/db.js @@ -0,0 +1,19 @@ +const sqlite3 = require('sqlite3').verbose(); +const logger = require('./../utils/logger'); +const path = require('path'); +const dbPath = path.join(__dirname, '../data/database.db'); + +const db = new sqlite3.Database(dbPath, (err) => { + if (err) { + logger.error('Error opening database:', err.message); + } else { + db.run(`CREATE TABLE IF NOT EXISTS data ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + info TEXT NOT NULL, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP + )`); + logger.info('Database created / opened succesfully'); + } +}); + +module.exports = db; \ No newline at end of file diff --git a/config/dockerConfig.json b/config/dockerConfig.json new file mode 100644 index 0000000..9ec4caf --- /dev/null +++ b/config/dockerConfig.json @@ -0,0 +1,9 @@ +{ + "hosts": [ + { + "name": "Fin-2", + "url": "100.89.35.135", + "port": "2375" + } + ] +} diff --git a/config/hosts.yaml b/config/hosts.yaml deleted file mode 100644 index d40e669..0000000 --- a/config/hosts.yaml +++ /dev/null @@ -1,24 +0,0 @@ -mintimeout: 10000 # The minimum time to wait before querying the same server again, defaults to 5000 Ms - -log: - logsize: 10 # Specify the Size of the log files in MB, default is 1MB - LogCount: 1 # How many log files should be kept in rotation. Default is 5 - -tags: - raspberry: red-200 - private: violet-400 - -hosts: - YourHost1: - url: hetzner - port: 2375 - - YourHost2: - url: 100.78.180.21 - port: 2375 - -container: - dozzle: # Container name - link: https://github.com - icon: minecraft.png - tags: private,raspberry diff --git a/config/loggerConfig.js b/config/loggerConfig.js new file mode 100644 index 0000000..7950348 --- /dev/null +++ b/config/loggerConfig.js @@ -0,0 +1,16 @@ +const { format } = require('winston'); + +module.exports = { + level: 'info', + format: format.combine( + format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + format.printf(({ timestamp, level, message }) => `${timestamp} [${level.toUpperCase()}]: ${message}`) + ), + transports: { + console: true, + file: { + enabled: true, + filename: 'logs/app.log', + }, + }, +}; diff --git a/config/swaggerConfig.js b/config/swaggerConfig.js new file mode 100644 index 0000000..c79ae47 --- /dev/null +++ b/config/swaggerConfig.js @@ -0,0 +1,28 @@ +const options = { + definition: { + openapi: '3.0.0', + info: { + title: 'Your API Documentation', + version: '1.0.0', + description: 'API documentation with authentication', + }, + components: { + securitySchemes: { + passwordAuth: { + type: 'apiKey', + in: 'header', + name: 'x-password', + description: 'Password required for authentication', + }, + }, + }, + security: [ + { + passwordAuth: [], + }, + ], + }, + apis: ['./routes/*/*.js'], // Point to your route files +}; + +module.exports = options; diff --git a/controllers/containerController.js b/controllers/containerController.js new file mode 100644 index 0000000..f62ec5c --- /dev/null +++ b/controllers/containerController.js @@ -0,0 +1,49 @@ +const fs = require("fs"); +const path = require("path"); +const { getDockerClient } = require("../utils/dockerClient"); +const logger = require("../utils/logger"); + +const getContainers = async (req, res) => { + const host = req.query.host || "local"; + logger.info(`Fetching containers from host: ${host}`); + try { + const docker = getDockerClient(host); + const containers = await docker.listContainers(); + + res.status(200).json(containers); + } catch (err) { + logger.error( + `Error fetching containers from host: ${host} - ${err.message || "Unknown error"} - Full error: ${JSON.stringify(err, null, 2)}`, + ); + res.status(500).json({ + error: `Error fetching containers: ${err.message || "Unknown error"}`, + }); + } +}; + +const getContainerStats = async (containerID, containerHost) => { + logger.info( + `Fetching stats for container: ${containerID} from host: ${containerHost}`, + ); + try { + const docker = getDockerClient(containerHost); + const container = docker.getContainer(containerID); + const stats = await container.stats({ stream: false }); + logger.info( + `Successfully fetched stats for container: ${containerID} from host: ${containerHost}`, + ); + res.status(200).json(stats); + } catch (error) { + logger.error( + `Error fetching stats for container: ${containerID} from host: ${containerHost} - ${error.message}`, + ); + res + .status(500) + .json({ error: `Error fetching container stats: ${error.message}` }); + } +}; + +module.exports = { + getContainers, + getContainerStats, +}; diff --git a/controllers/fetchData.js b/controllers/fetchData.js new file mode 100644 index 0000000..43b4f8a --- /dev/null +++ b/controllers/fetchData.js @@ -0,0 +1,34 @@ +const db = require("../config/db"); +const { fetchAllContainers } = require("../utils/containerService"); +const logger = require("./../utils/logger"); +const path = require("path"); +const fs = require("fs"); + +const fetchData = async () => { + try { + const allContainerData = await fetchAllContainers(); + const data = allContainerData; + + if (process.env.OFFLINE === "true") { + logger.info("No new data inserted --- OFFLINE MODE"); + } else { + // Insert data into the SQLite database + db.run( + `INSERT INTO data (info) VALUES (?)`, + [JSON.stringify(data)], + function (error) { + if (error) { + logger.info("Error inserting data:", error.message); + console.error("Error inserting data:", error.message); + return; + } + logger.info(`Data inserted with ID: ${this.lastID}`); + }, + ); + } + } catch (error) { + logger.error("Error fetching data:", error.message); + } +}; + +module.exports = fetchData; diff --git a/controllers/frontendConfiguration.js b/controllers/frontendConfiguration.js new file mode 100644 index 0000000..ff1ce3e --- /dev/null +++ b/controllers/frontendConfiguration.js @@ -0,0 +1,180 @@ +const fs = require("fs"); +const path = require("path"); +const dataPath = path.join(__dirname, "../data/frontendConfiguration.json"); +const logger = require("../utils/logger"); + +async function hideContainer(containerName) { + try { + let data = await readData(); + const containerIndex = data.findIndex( + (container) => container.name === containerName, + ); + + if (containerIndex !== -1) { + data[containerIndex].hidden = true; + await saveData(data); + } else { + data.push({ name: containerName, hidden: true }); + await saveData(data); + } + } catch (error) { + logger.error(error); + } +} + +async function unhideContainer(containerName) { + try { + let data = await readData(); + const containerIndex = data.findIndex( + (container) => container.name === containerName, + ); + + if (containerIndex !== -1) { + delete data[containerIndex].hidden; + await saveData(data); + cleanupData(); + } + } catch (error) { + logger.error(error); + } +} + +async function addTagToContainer(containerName, tag) { + try { + let data = await readData(); + const containerIndex = data.findIndex( + (container) => container.name === containerName, + ); + + if (containerIndex !== -1) { + if (!data[containerIndex].tags) { + data[containerIndex].tags = []; + } + data[containerIndex].tags.push(tag); + await saveData(data); + } else { + data.push({ name: containerName, tags: [tag] }); + await saveData(data); + } + } catch (error) { + logger.error(error); + } +} + +async function removeTagFromContainer(containerName, tag) { + try { + let data = await readData(); + const containerIndex = data.findIndex( + (container) => container.name === containerName, + ); + + if (containerIndex !== -1 && data[containerIndex].tags) { + data[containerIndex].tags = data[containerIndex].tags.filter( + (t) => t !== tag, + ); + await saveData(data); + cleanupData(); + } + } catch (error) { + logger.error(error); + } +} + +async function pinContainer(containerName) { + try { + let data = await readData(); + const containerIndex = data.findIndex( + (container) => container.name === containerName, + ); + + if (containerIndex !== -1) { + data[containerIndex].pinned = true; + await saveData(data); + } else { + data.push({ name: containerName, pinned: true }); + await saveData(data); + } + } catch (error) { + logger.error(error); + } +} + +async function unpinContainer(containerName) { + try { + let data = await readData(); + const containerIndex = data.findIndex( + (container) => container.name === containerName, + ); + + if (containerIndex !== -1) { + delete data[containerIndex].pinned; + await saveData(data); + cleanupData(); + } + } catch (error) { + logger.error(error); + } +} + +async function readData() { + try { + const data = await fs.promises.readFile(dataPath, "utf-8"); + return JSON.parse(data); + } catch (error) { + console.error("readData"); + if (error.code === "ENOENT") { + await saveData([]); + return []; + } else { + throw error; + } + } +} + +async function saveData(data) { + try { + await fs.promises.writeFile( + dataPath, + JSON.stringify(data, null, 2), + "utf-8", + ); + logger.info("Succesfully wrote to file"); + } catch (error) { + logger.error(error); + } +} + +async function cleanupData() { + try { + const data = await readData(); + let cleanedData = []; + + if (data && Array.isArray(data)) { + cleanedData = data.filter((container) => { + // Filter out containers with empty "tags" or containers with only one property (name) + if ( + container.tags && + Array.isArray(container.tags) && + container.tags.length === 0 + ) { + delete container.tags; + } + return Object.keys(container).length > 1; + }); + } + + await saveData(cleanedData); + } catch (error) { + logger.error(error); + } +} + +module.exports = { + hideContainer, + unhideContainer, + addTagToContainer, + removeTagFromContainer, + pinContainer, + unpinContainer, + cleanupData, +}; diff --git a/controllers/scheduler.js b/controllers/scheduler.js new file mode 100644 index 0000000..5bb3ca7 --- /dev/null +++ b/controllers/scheduler.js @@ -0,0 +1,75 @@ +const fetchData = require("./fetchData"); +const logger = require("../utils/logger"); +const db = require("../config/db"); + +let fetchInterval = 5 * 60 * 1000; +let intervalId; + +const scheduleFetch = () => { + fetchData().then(() => { + cleanupOldEntries(); + }); + intervalId = setInterval(() => { + logger.info( + `Fetching data at interval of ${fetchInterval / 1000} seconds.`, + ); + cleanupOldEntries(); + fetchData(); + }, fetchInterval); + logger.info(`Data fetching scheduled every ${fetchInterval / 1000} seconds.`); +}; + +const setFetchInterval = (newInterval) => { + if (intervalId) { + clearInterval(intervalId); + logger.info(`Cleared existing fetch interval.`); + } + fetchInterval = newInterval; + scheduleFetch(); + logger.info(`Fetch interval updated to ${fetchInterval / 1000} seconds.`); +}; + +const parseInterval = (interval) => { + const timeUnits = { + s: 1000, + m: 60 * 1000, + h: 60 * 60 * 1000, + }; + + let totalMilliseconds = 0; + const regex = /(\d+)([smh])/g; + let match; + + while ((match = regex.exec(interval))) { + const value = parseInt(match[1], 10); + const unit = match[2]; + totalMilliseconds += value * timeUnits[unit]; + } + + return totalMilliseconds; +}; + +const getCurrentSchedule = () => { + return { + interval: fetchInterval / 1000, + }; +}; + +const cleanupOldEntries = async () => { + const twentyFourHoursAgo = new Date( + Date.now() - 24 * 60 * 60 * 1000, + ).toISOString(); + try { + await db.run("DELETE FROM data WHERE timestamp < ?", twentyFourHoursAgo); + logger.info(`Old entries cleared from the database.`); + } catch (error) { + logger.error(`Error clearing old entries: ${error.message}`); + } +}; + +module.exports = { + scheduleFetch, + setFetchInterval, + parseInterval, + getCurrentSchedule, +}; diff --git a/data/database.db b/data/database.db new file mode 100644 index 0000000000000000000000000000000000000000..80295980fc1caba1cff16d304197ec55893652f2 GIT binary patch literal 16384 zcmeI2-ELdQ6@?`ytyh63@AMkC zVd_0QK0W;1;Yshs$+M$_lUKdpAHM1xygdE<*)b*`9Uh-keRg!%`}FX$gO|@wd!M{KIl=YL>cKBg4~|~kUigFh-F)|P z+`(^m>j-oNIszSmjzCA?zenJ2zuEoS&fjFfSeVTs*5o8cM$jMvj?9?#C& z1DZz{12N#+$@J@^NxF;DS<1yL zWnLK1wUe5-6DU!{}kDB z;5xM13EQJvQq$7wz>3ulY+@|j7f)G=l5HR>WiUC~XqnTg=&Z>U%QEMQWkiK$VvB%l zQ&XBdC(YvEJ=6pv=V(O@A$SL}<*0RJH>0=V_5Jr3c}Yuol{rgZNA3+pW${DSvqVNn zS3(ISlR+j*A}xdhP7{K&DPr zbI+QZ5*$pMv}!UpM>o&4XKhaN61C}kny;2TwNw-x2vRDjV*-h&3sZzj7m>448dNC7}v}=9huOzY;Iic48w;rkf>>nX=7L$sL~E;Z0!x@d%_Xq72z{k1tdzc zcK9^zzbLD9pfZ%7DRs(BJ!_S|LQ6 zoN7QeMmJB@cx=piV}{4)!}04E)5+`aj{F#_;B*;~D1E%>kP?p;3yJj^tA))zdm*EX z9IrTQn_>`-nZhk&UMg$7%LM#6Y{63_2Am#XS;vZMEy)&6h2@J7>EgkFBx96eR+&bP z#MJ}|VNf8j#d&-F{r}y6?%=n(bp$#B9f6KON1!9n5$FhX1Udp8fsQ~&pd;}AMd0!7 zTTgyb+Y;*A`iZpC)D70Pm4<#$h5k)t`l|CGqQQSJ!5lOOR;~8fV&oth-g|P&*O>iuOVOiAR^h! zZQ)74`&1EfzsTEICWv^FoCYxCIN@QToi2jaFhr0x52w8lww4eEKWD`k;R=_}O1dNa zTNBptHYaXQci{&tqgJI%(S!)NE~2_=Pa$w0fiUX~56lpEsfxiim|}n)(sghy_>fHA z&WADpDcwN9MdFV5g5LrvtPl%9th z8BymGj~rdWbjlZnj2UFMu- z=-^x?bb*JYl|=AGq^wKDN&)9MA;X`@0Sz{wzMy(IcM`gTmm(Hl5B^pDVjf$a2dYj?wNydXS5)NcZ3jG+2MZ>nhs^&4t2DS5~ z#$=l+!E&G1z6V-F;Ct7}hBy3W^aa80MX{H-W|QDNLK_pB=W3X?CT@;*HoeYqG8*~u z?G_Zyd@81(gt3CHhU5vwU&8v$NtGlSiG>9DPLz)v-Bt}J+n1RKJZML9`inv&B zWc`E?XAirgycS$6z73EP4OZAYNo-e}s47Nk3~!#R7#q_zhjO$EC_Sn`q*`n^=qg1kqQ4FXWWc#hdfhZ2e!$_RZY=mv|}u2Mn(ZxNqujs}H< ztbUkdEfKQWw1g!^NbCgesFt^5*9kV}Y))`IE8i`-F;qkoYipcEcN&9TkClg`he`?N zGK&(cC~4*tJbR0F1i@5bpoXl*BE`2Umu+qwEfDxl@Gv)n78YabqKtPu|MqB5j+`=g g=J0atI=I!GH|6%_kAexc&-p&%J!YRWfv>Iq0`|MTqyPW_ literal 0 HcmV?d00001 diff --git a/dockstatapi.js b/dockstatapi.js deleted file mode 100644 index d06fb1e..0000000 --- a/dockstatapi.js +++ /dev/null @@ -1,379 +0,0 @@ -const express = require('express'); -const path = require('path'); -const yaml = require('yamljs'); -const Docker = require('dockerode'); -const cors = require('cors'); -const fs = require('fs'); -const { exec } = require('child_process'); -const logger = require('./logger'); -const updateAvailable = require('./modules/updateAvailable') -const app = express(); -const port = 7070; -const key = process.env.SECRET || 'CHANGE-ME'; -const skipAuth = process.env.SKIP_AUTH || 'True' -const cupUrl = process.env.CUP_URL || 'null' - -let config = yaml.load('./config/hosts.yaml'); -let hosts = config.hosts; -let containerConfigs = config.container || {}; -let maxlogsize = config.log.logsize || 1; -let LogAmount = config.log.LogCount || 5; -let queryInterval = config.mintimeout || 5000; -let latestStats = {}; -let hostQueues = {}; -let previousNetworkStats = {}; -let generalStats = {}; -let previousContainerStates = {}; -let previousRunningContainers = {}; - - -app.use(cors()); -app.use(express.json()); - -const authenticateHeader = (req, res, next) => { - const authHeader = req.headers['authorization']; - - if (skipAuth === 'True') { - next(); - } else { - if (!authHeader || authHeader !== key) { - logger.error(`${authHeader} != ${key}`); - return res.status(401).json({ error: "Unauthorized" }); - } - else { - next(); - } - } -}; - -function createDockerClient(hostConfig) { - return new Docker({ - host: hostConfig.url, - port: hostConfig.port, - }); -} - -function getTagColor(tag) { - const tagsConfig = config.tags || {}; - return tagsConfig[tag] || ''; -} - -async function getContainerStats(docker, containerId) { - const container = docker.getContainer(containerId); - return new Promise((resolve, reject) => { - container.stats({ stream: false }, (err, stats) => { - if (err) return reject(err); - resolve(stats); - }); - }); -} - -async function handleContainerStateChanges(hostName, currentContainers) { - const currentRunningContainers = currentContainers - .filter(container => container.state === 'running') - .reduce((map, container) => { - map[container.id] = container; - return map; - }, {}); - - const previousHostContainers = previousRunningContainers[hostName] || {}; - - // Check for containers that have been removed or exited - for (const containerId of Object.keys(previousHostContainers)) { - const container = previousHostContainers[containerId]; - if (!currentRunningContainers[containerId]) { - if (container.state === 'running') { - // Container removed - exec(`bash ./scripts/notify.sh REMOVE ${containerId} ${container.name} ${hostName} ${container.state}`, (error, stdout, stderr) => { - if (error) { - logger.error(`Error executing REMOVE notify.sh: ${error.message}`); - } else { - logger.info(`Container removed: ${container.name} (${containerId}) from host ${hostName}`); - logger.info(stdout); - } - }); - } - else if (container.state === 'exited') { - // Container exited - exec(`bash ./scripts/notify.sh EXIT ${containerId} ${container.name} ${hostName} ${container.state}`, (error, stdout, stderr) => { - if (error) { - logger.error(`Error executing EXIT notify.sh: ${error.message}`); - } else { - logger.info(`Container exited: ${container.name} (${containerId}) from host ${hostName}`); - logger.info(stdout); - } - }); - } - } - } - - // Check for new containers or state changes - for (const containerId of Object.keys(currentRunningContainers)) { - const container = currentRunningContainers[containerId]; - const previousContainer = previousHostContainers[containerId]; - - if (!previousContainer) { - // New container added - exec(`bash ./scripts/notify.sh ADD ${containerId} ${container.name} ${hostName} ${container.state}`, (error, stdout, stderr) => { - if (error) { - logger.error(`Error executing ADD notify.sh: ${error.message}`); - } else { - logger.info(`Container added: ${container.name} (${containerId}) to host ${hostName}`); - logger.info(stdout); - } - }); - } else if (previousContainer.state !== container.state) { - // Container state has changed - const newState = container.state; - if (newState === 'exited') { - exec(`bash ./scripts/notify.sh EXIT ${containerId} ${container.name} ${hostName} ${newState}`, (error, stdout, stderr) => { - if (error) { - logger.error(`Error executing EXIT notify.sh: ${error.message}`); - } else { - logger.info(`Container exited: ${container.name} (${containerId}) from host ${hostName}`); - logger.info(stdout); - } - }); - } else { - // Any other state change - exec(`bash ./scripts/notify.sh ANY ${containerId} ${container.name} ${hostName} ${newState}`, (error, stdout, stderr) => { - if (error) { - logger.error(`Error executing ANY notify.sh: ${error.message}`); - } else { - logger.info(`Container state changed to ${newState}: ${container.name} (${containerId}) from host ${hostName}`); - logger.info(stdout); - } - }); - } - } - } - - // Update the previous state for the next comparison - previousRunningContainers[hostName] = currentRunningContainers; -} - -async function queryHostStats(hostName, hostConfig) { - logger.debug(`Querying Docker stats for host: ${hostName} (${hostConfig.url}:${hostConfig.port})`); - - const docker = createDockerClient(hostConfig); - - try { - const info = await docker.info(); - const totalMemory = info.MemTotal; - const totalCPUs = info.NCPU; - const containers = await docker.listContainers({ all: true }); - - const statsPromises = containers.map(async (container) => { - try { - const containerName = container.Names[0].replace('/', ''); - const containerState = container.State; - const updateAvailableFlag = await updateAvailable(container.Image, cupUrl); - let networkMode = container.HostConfig.NetworkMode; - - // Check if network mode is in the format "container:IDXXXXXXXX" - if (networkMode.startsWith("container:")) { - const linkedContainerId = networkMode.split(":")[1]; - const linkedContainer = await docker.getContainer(linkedContainerId).inspect(); - const linkedContainerName = linkedContainer.Name.replace('/', ''); // Remove leading slash - - networkMode = `Container: ${linkedContainerName}`; // Format the network mode - } - - if (containerState !== 'running') { - previousContainerStates[container.Id] = containerState; - return { - name: containerName, - id: container.Id, - hostName: hostName, - state: containerState, - image: container.Image, - update_available: updateAvailableFlag || false, - cpu_usage: 0, - mem_usage: 0, - mem_limit: 0, - net_rx: 0, - net_tx: 0, - current_net_rx: 0, - current_net_tx: 0, - networkMode: networkMode, - link: containerConfigs[containerName]?.link || '', - icon: containerConfigs[containerName]?.icon || '', - tags: getTagColor(containerConfigs[containerName]?.tags || ''), - }; - } - - // Fetch container stats for running containers - const containerStats = await getContainerStats(docker, container.Id); - const containerCpuUsage = containerStats.cpu_stats.cpu_usage.total_usage; - const containerMemoryUsage = containerStats.memory_stats.usage; - - let netRx = 0, netTx = 0, currentNetRx = 0, currentNetTx = 0; - - if (networkMode !== 'host' && containerStats.networks?.eth0) { - const previousStats = previousNetworkStats[container.Id] || { rx_bytes: 0, tx_bytes: 0 }; - currentNetRx = containerStats.networks.eth0.rx_bytes - previousStats.rx_bytes; - currentNetTx = containerStats.networks.eth0.tx_bytes - previousStats.tx_bytes; - - previousNetworkStats[container.Id] = { - rx_bytes: containerStats.networks.eth0.rx_bytes, - tx_bytes: containerStats.networks.eth0.tx_bytes, - }; - - netRx = containerStats.networks.eth0.rx_bytes; - netTx = containerStats.networks.eth0.tx_bytes; - } - - previousContainerStates[container.Id] = containerState; - const config = containerConfigs[containerName] || {}; - - const tagArray = (config.tags || '') - .split(',') - .map(tag => { - const color = getTagColor(tag); - return color ? `${tag}:${color}` : tag; - }) - .join(','); - - return { - name: containerName, - id: container.Id, - hostName: hostName, - image: container.Image, - update_available: updateAvailableFlag || false, - state: containerState, - cpu_usage: containerCpuUsage, - mem_usage: containerMemoryUsage, - mem_limit: containerStats.memory_stats.limit, - net_rx: netRx, - net_tx: netTx, - current_net_rx: currentNetRx, - current_net_tx: currentNetTx, - networkMode: networkMode, - link: config.link || '', - icon: config.icon || '', - tags: tagArray, - }; - } catch (err) { - logger.error(`Failed to fetch stats for container ${container.Names[0]} (${container.Id}): ${err.message}`); - return null; - } - }); - - const hostStats = await Promise.all(statsPromises); - const validStats = hostStats.filter(stat => stat !== null); - - const totalCpuUsage = validStats.reduce((acc, container) => acc + parseFloat(container.cpu_usage), 0); - const totalMemoryUsage = validStats.reduce((acc, container) => acc + container.mem_usage, 0); - const memoryUsagePercent = ((totalMemoryUsage / totalMemory) * 100).toFixed(2); - - generalStats[hostName] = { - containerCount: validStats.length, - totalCPUs: totalCPUs, - totalMemory: totalMemory, - cpuUsage: totalCpuUsage, - memoryUsage: memoryUsagePercent, - }; - - latestStats[hostName] = validStats; - - logger.debug(`Fetched stats for ${validStats.length} containers from ${hostName}`); - - // Handle container state changes - await handleContainerStateChanges(hostName, validStats); - } catch (err) { - logger.error(`Failed to fetch containers from ${hostName}: ${err.message}`); - } -} - - -async function handleHostQueue(hostName, hostConfig) { - while (true) { - await queryHostStats(hostName, hostConfig); - await new Promise(resolve => setTimeout(resolve, queryInterval)); - } -} - -// Initialize the host queues -function initializeHostQueues() { - for (const [hostName, hostConfig] of Object.entries(hosts)) { - hostQueues[hostName] = handleHostQueue(hostName, hostConfig); - } -} - -// Dynamically reloads the yaml file -function reloadConfig() { - for (const hostName in hostQueues) { - hostQueues[hostName] = null; - } - try { - config = yaml.load('./config/hosts.yaml'); - hosts = config.hosts; - containerConfigs = config.container || {}; - maxlogsize = config.log.logsize || 1; - LogAmount = config.log.LogCount || 5; - queryInterval = config.mintimeout || 5000; - - logger.info('Configuration reloaded successfully.'); - - initializeHostQueues(); - } catch (err) { - logger.error(`Failed to reload configuration: ${err.message}`); - } -} - -// Watch the YAML file for changes and reload the config -fs.watchFile('./config/hosts.yaml', (curr, prev) => { - if (curr.mtime !== prev.mtime) { - logger.info('Detected change in configuration file. Reloading...'); - reloadConfig(); - } -}); - -// Endpoint to get stats -app.get('/stats', authenticateHeader, (req, res) => { - res.json(latestStats); -}); - -// Endpoint for general Host based statistics -app.get('/hosts', authenticateHeader, (req, res) => { - res.json(generalStats); -}); - -// Read Only config endpoint -app.get('/config', authenticateHeader, (req, res) => { - const filePath = path.join(__dirname, './config/hosts.yaml'); - res.set('Content-Type', 'text/plain'); // Keep as plain text - fs.readFile(filePath, 'utf8', (err, data) => { - logger.debug('Requested config file: ' + filePath); - if (err) { - logger.error(err); - res.status(500).send('Error reading file'); - } else { - res.send(data); - } - }); -}); - -app.get('/', (req, res) => { - res.redirect(301, '/stats'); -}); - -app.get('/status', (req, res) => { - logger.info("Healthcheck requested"); - return res.status(200).send('UP'); -}); - -// Start the server and log the startup message -app.listen(port, () => { - logger.info('=============================== DockStat ===============================') - logger.info(`DockStatAPI is running on http://localhost:${port}/stats`); - logger.info(`Minimum timeout between stats queries is: ${queryInterval} milliseconds`); - logger.info(`The max size for Log files is: ${maxlogsize}MB`) - logger.info(`The amount of log files to keep is: ${LogAmount}`); - logger.info(`Secret Key: ${key}`) - logger.info(`Cup URL: ${cupUrl}`) - logger.info("Press Ctrl+C to stop the server."); - logger.info('========================================================================') -}); - -initializeHostQueues(); diff --git a/entrypoint.sh b/entrypoint.sh index df95b98..2008cdb 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,7 +1,26 @@ -#!/usr/bin/env bash +#!/bin/bash -SECRET="${SECRET//\"}" +cat << EOF +Welcome to -export SECRET + ###### ###### #### ### ### #### ######### ###### ######### + ### ### ### ### ### ### ### ### ### ### ### ### + ### ### ### ### ### ###### #### ### ### ### ### + ### ### ### ### ### ### ### #### ### ############ ### + ### ### ### ### ### ### ### #### ### ### ### ### + ###### ###### #### ### ### #### ### ### ### ### (API) -exec npm run start \ No newline at end of file +Useful links: + +- Documentation: https://outline.itsnik.de/s/dockstat +- GitHub (Frontend): https://github.com/its4nik/dockstat +- GitHub (Backend): https://github.com/its4nik/dockstatapi +- API Documentation: http://localhost:7000/api-docs + +Summary: + +DockStat and DockStatAPI are 2 fully OpenSource projects, DockStatAPI is a simple but extensible API which allows queries via a REST endpoint. + +EOF + +npm run start diff --git a/logger.js b/logger.js deleted file mode 100644 index ebaacc3..0000000 --- a/logger.js +++ /dev/null @@ -1,24 +0,0 @@ -const winston = require('winston'); -const yaml = require('yamljs'); -const config = yaml.load('./config/hosts.yaml'); - -const maxlogsize = config.log.logsize || 1; -const LogAmount = config.log.LogCount || 5; - -const logger = winston.createLogger({ - level: 'debug', - format: winston.format.combine( - winston.format.timestamp(), - winston.format.json() - ), - transports: [ - new winston.transports.Console(), - new winston.transports.File({ - filename: './logs/dockstat.log', - maxsize: 1024 * 1024 * maxlogsize, - maxFiles: LogAmount - }) - ] -}); - -module.exports = logger; \ No newline at end of file diff --git a/middleware/authMiddleware.js b/middleware/authMiddleware.js new file mode 100644 index 0000000..8ee6a68 --- /dev/null +++ b/middleware/authMiddleware.js @@ -0,0 +1,50 @@ +const bcrypt = require("bcrypt"); +const fs = require("fs"); +const path = require("path"); +const logger = require("../utils/logger"); +const passwordFile = path.join(__dirname, "password.json"); +const passwordBool = path.join(__dirname, "usePassword.txt"); + +function authMiddleware(req, res, next) { + fs.readFile(passwordBool, "utf8", (err, data) => { + if (err) { + logger.error("Error reading the file:", err); + return; + } + + const isAuthEnabled = data.trim() === "true"; + + if (!isAuthEnabled) { + return next(); + } + + const providedPassword = req.headers["x-password"]; + if (!providedPassword) { + logger.error("Password required - Denied"); + return res.status(401).json({ message: "Password required" }); + } + + fs.readFile(passwordFile, "utf8", (err, data) => { + if (err) { + logger.error("Error reading password"); + return res.status(500).json({ message: "Error reading password" }); + } + + const storedData = JSON.parse(data); + bcrypt.compare(providedPassword, storedData.hash, (err, result) => { + if (err) { + logger.error("Error validating password - Denied access"); + return res.status(500).json({ message: "Error validating password" }); + } + if (!result) { + console.error("Invalid Password - Denied access"); + return res.status(401).json({ message: "Invalid password" }); + } + + next(); + }); + }); + }); +} + +module.exports = authMiddleware; diff --git a/middleware/password.json b/middleware/password.json new file mode 100644 index 0000000..37a7c4c --- /dev/null +++ b/middleware/password.json @@ -0,0 +1 @@ +{"hash":"$2b$10$qGcNmciEGhX.PiB.ofHib.Fob.nOjQNfguBoD4JDbbbTysrLrKGEi","salt":"$2b$10$qGcNmciEGhX.PiB.ofHib."} \ No newline at end of file diff --git a/middleware/usePassword.txt b/middleware/usePassword.txt new file mode 100644 index 0000000..02e4a84 --- /dev/null +++ b/middleware/usePassword.txt @@ -0,0 +1 @@ +false \ No newline at end of file diff --git a/misc/dependencyGraphs/mermaid-all.txt b/misc/dependencyGraphs/mermaid-all.txt new file mode 100644 index 0000000..87ba7da --- /dev/null +++ b/misc/dependencyGraphs/mermaid-all.txt @@ -0,0 +1,70 @@ +flowchart LR + +subgraph 0["config"] +1["db.js"] +2["swaggerConfig.js"] +9["dockerConfig.json"] +end +subgraph 3["controllers"] +4["containerController.js"] +7["fetchData.js"] +A["frontendConfiguration.js"] +B["scheduler.js"] +end +subgraph 5["utils"] +6["dockerClient.js"] +8["containerService.js"] +N["extractHostData.js"] +O["writeOfflineLog.js"] +U["rateLimiter.js"] +end +subgraph C["middleware"] +D["authMiddleware.js"] +end +subgraph E["routes"] +subgraph F["auth"] +G["routes.js"] +end +subgraph H["data"] +I["routes.js"] +end +subgraph J["frontendController"] +K["routes.js"] +end +subgraph L["getter"] +M["routes.js"] +end +subgraph P["setter"] +Q["routes.js"] +end +end +R["server.js"] +subgraph S["swagger"] +T["swaggerDocs.js"] +end +4-->6 +7-->1 +7-->8 +8-->9 +8-->6 +B-->1 +B-->7 +I-->1 +K-->A +M-->9 +M-->B +M-->8 +M-->6 +M-->N +M-->O +Q-->B +R-->B +R-->D +R-->G +R-->I +R-->K +R-->M +R-->Q +R-->T +R-->U +T-->2 diff --git a/misc/dependencyGraphs/mermaid-api.txt b/misc/dependencyGraphs/mermaid-api.txt new file mode 100644 index 0000000..c2dd6c8 --- /dev/null +++ b/misc/dependencyGraphs/mermaid-api.txt @@ -0,0 +1,33 @@ +flowchart LR + +subgraph 0["routes"] +subgraph 1["getter"] +2["routes.js"] +end +end +subgraph 3["config"] +4["dockerConfig.json"] +7["db.js"] +end +subgraph 5["controllers"] +6["scheduler.js"] +8["fetchData.js"] +end +subgraph 9["utils"] +A["containerService.js"] +B["dockerClient.js"] +C["extractHostData.js"] +D["writeOfflineLog.js"] +end +2-->4 +2-->6 +2-->A +2-->B +2-->C +2-->D +6-->7 +6-->8 +8-->7 +8-->A +A-->4 +A-->B diff --git a/misc/dependencyGraphs/mermaid-auth.txt b/misc/dependencyGraphs/mermaid-auth.txt new file mode 100644 index 0000000..e7ab066 --- /dev/null +++ b/misc/dependencyGraphs/mermaid-auth.txt @@ -0,0 +1,8 @@ +flowchart LR + +subgraph 0["routes"] +subgraph 1["auth"] +2["routes.js"] +end +end + diff --git a/misc/dependencyGraphs/mermaid-conf.txt b/misc/dependencyGraphs/mermaid-conf.txt new file mode 100644 index 0000000..65e4b74 --- /dev/null +++ b/misc/dependencyGraphs/mermaid-conf.txt @@ -0,0 +1,26 @@ +flowchart LR + +subgraph 0["routes"] +subgraph 1["setter"] +2["routes.js"] +end +end +subgraph 3["controllers"] +4["scheduler.js"] +7["fetchData.js"] +end +subgraph 5["config"] +6["db.js"] +A["dockerConfig.json"] +end +subgraph 8["utils"] +9["containerService.js"] +B["dockerClient.js"] +end +2-->4 +4-->6 +4-->7 +7-->6 +7-->9 +9-->A +9-->B diff --git a/misc/dependencyGraphs/mermaid-data.txt b/misc/dependencyGraphs/mermaid-data.txt new file mode 100644 index 0000000..e212edc --- /dev/null +++ b/misc/dependencyGraphs/mermaid-data.txt @@ -0,0 +1,11 @@ +flowchart LR + +subgraph 0["routes"] +subgraph 1["data"] +2["routes.js"] +end +end +subgraph 3["config"] +4["db.js"] +end +2-->4 diff --git a/misc/dependencyGraphs/mermaid-frontend.txt b/misc/dependencyGraphs/mermaid-frontend.txt new file mode 100644 index 0000000..35b4e61 --- /dev/null +++ b/misc/dependencyGraphs/mermaid-frontend.txt @@ -0,0 +1,11 @@ +flowchart LR + +subgraph 0["routes"] +subgraph 1["frontendController"] +2["routes.js"] +end +end +subgraph 3["controllers"] +4["frontendConfiguration.js"] +end +2-->4 diff --git a/misc/entrypoint.sh b/misc/entrypoint.sh new file mode 100644 index 0000000..2008cdb --- /dev/null +++ b/misc/entrypoint.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +cat << EOF +Welcome to + + ###### ###### #### ### ### #### ######### ###### ######### + ### ### ### ### ### ### ### ### ### ### ### ### + ### ### ### ### ### ###### #### ### ### ### ### + ### ### ### ### ### ### ### #### ### ############ ### + ### ### ### ### ### ### ### #### ### ### ### ### + ###### ###### #### ### ### #### ### ### ### ### (API) + +Useful links: + +- Documentation: https://outline.itsnik.de/s/dockstat +- GitHub (Frontend): https://github.com/its4nik/dockstat +- GitHub (Backend): https://github.com/its4nik/dockstatapi +- API Documentation: http://localhost:7000/api-docs + +Summary: + +DockStat and DockStatAPI are 2 fully OpenSource projects, DockStatAPI is a simple but extensible API which allows queries via a REST endpoint. + +EOF + +npm run start diff --git a/modules/updateAvailable.js b/modules/updateAvailable.js deleted file mode 100644 index 1a25ce3..0000000 --- a/modules/updateAvailable.js +++ /dev/null @@ -1,32 +0,0 @@ -const logger = require('../logger'); - -async function getData(target, url) { - - if (url === 'null') { - return false; - } - else { - try { - const response = await fetch(`${url}/json`, { - method: "GET" - }); - if (!response.ok) { - throw new Error(`Response status: ${response.status}`); - } - - const json = await response.json(); - - const images = json.images; - - for (const image in images) { - if (target === image) { - return images.hasOwnProperty(target); - } - } - } catch (error) { - logger.error(error.message); - } - } -} - -module.exports = getData; diff --git a/package-lock.json b/package-lock.json index 37c8cf2..68d9374 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,13 +9,67 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "bcrypt": "^5.1.1", "child_process": "^1.0.2", "cors": "^2.8.5", "dockerode": "^4.0.2", - "express": "^4.21.0", + "express": "^4.21.1", + "express-rate-limit": "^7.4.1", "node-fetch": "^3.3.2", - "winston": "^3.14.2", + "python-shell": "^5.0.0", + "sqlite3": "^5.1.7", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1", + "winston": "^3.15.0", "yamljs": "^0.3.0" + }, + "devDependencies": { + "dependency-cruiser": "^16.5.0", + "nodemon": "^3.1.7" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz", + "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==", + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.6", + "call-me-maybe": "^1.0.1", + "js-yaml": "^4.1.0" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", + "license": "MIT" + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==", + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^9.0.6", + "@apidevtools/openapi-schemas": "^2.0.4", + "@apidevtools/swagger-methods": "^3.0.2", + "@jsdevtools/ono": "^7.1.3", + "call-me-maybe": "^1.0.1", + "z-schema": "^5.0.1" + }, + "peerDependencies": { + "openapi-types": ">=7" } }, "node_modules/@balena/dockerignore": { @@ -44,12 +98,161 @@ "kuler": "^2.0.0" } }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "license": "MIT", + "optional": true + }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "license": "MIT" + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "license": "MIT", + "optional": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, "node_modules/@types/triple-beam": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", "license": "MIT" }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -63,6 +266,178 @@ "node": ">= 0.6" } }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-jsx-walk": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/acorn-jsx-walk/-/acorn-jsx-walk-2.0.0.tgz", + "integrity": "sha512-uuo6iJj4D4ygkdzd6jPtcxs8vZgDX9YFIkqczGImoypX2fQ4dVImmu3UzA4ynixCIMTrEOWW+95M2HuBaCEOVA==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn-loose": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/acorn-loose/-/acorn-loose-8.4.0.tgz", + "integrity": "sha512-M0EUka6rb+QC4l9Z3T0nJEzNOO7JcoJlYMrBlyBCiFSXRyxjLKayd4TbQs2FDRWQU1h9FR7QVNHt+PEaoNL5rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agentkeepalive": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "license": "MIT", + "optional": true, + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "license": "MIT", + "optional": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -119,6 +494,20 @@ ], "license": "MIT" }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -128,6 +517,34 @@ "tweetnacl": "^0.14.3" } }, + "node_modules/bcrypt/node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -185,6 +602,19 @@ "concat-map": "0.0.1" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -226,6 +656,46 @@ "node": ">= 0.8" } }, + "node_modules/cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cacache/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=10" + } + }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -244,18 +714,99 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "license": "MIT" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/child_process": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/child_process/-/child_process-1.0.2.tgz", "integrity": "sha512-Wmza/JzL0SiWz7kl6MhIKT5ceIlnFPJX+lwUGj7Clhy5MMldsSoJR0+uvRzOS5Kv45Mq7t1PoE8TsOA9bzvb6g==", "license": "ISC" }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "license": "ISC" }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, "node_modules/color": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", @@ -266,11 +817,24 @@ "color-string": "^1.6.0" } }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" }, "node_modules/color-string": { "version": "1.9.1", @@ -282,6 +846,15 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, "node_modules/color/node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -307,12 +880,27 @@ "text-hex": "1.0.x" } }, + "node_modules/commander": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", + "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "license": "MIT" }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC" + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -334,9 +922,9 @@ } }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -401,6 +989,30 @@ } } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -417,6 +1029,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -425,6 +1043,71 @@ "node": ">= 0.8" } }, + "node_modules/dependency-cruiser": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dependency-cruiser/-/dependency-cruiser-16.5.0.tgz", + "integrity": "sha512-6IELC3qRumlwhnbPLmcOK6WWdiGPFBw9a+D8DUsnTFpZ81tEtkAud4OPmU3OJFcuWS5VpgvKlctFkby5XDsGzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.13.0", + "acorn-jsx": "^5.3.2", + "acorn-jsx-walk": "^2.0.0", + "acorn-loose": "^8.4.0", + "acorn-walk": "^8.3.4", + "ajv": "^8.17.1", + "commander": "^12.1.0", + "enhanced-resolve": "^5.17.1", + "ignore": "^6.0.2", + "interpret": "^3.1.1", + "is-installed-globally": "^1.0.0", + "json5": "^2.2.3", + "memoize": "^10.0.0", + "picocolors": "^1.1.1", + "picomatch": "^4.0.2", + "prompts": "^2.4.2", + "rechoir": "^0.8.0", + "safe-regex": "^2.1.1", + "semver": "^7.6.3", + "teamcity-service-messages": "^0.1.14", + "tsconfig-paths-webpack-plugin": "^4.1.0", + "watskeburt": "^4.1.0" + }, + "bin": { + "depcruise": "bin/dependency-cruise.mjs", + "depcruise-baseline": "bin/depcruise-baseline.mjs", + "depcruise-fmt": "bin/depcruise-fmt.mjs", + "depcruise-wrap-stream-in-html": "bin/wrap-stream-in-html.mjs", + "dependency-cruise": "bin/dependency-cruise.mjs", + "dependency-cruiser": "bin/dependency-cruise.mjs" + }, + "engines": { + "node": "^18.17||>=20" + } + }, + "node_modules/dependency-cruiser/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/dependency-cruiser/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -434,6 +1117,15 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/docker-modem": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.3.tgz", @@ -463,11 +1155,29 @@ "node": ">= 8.0" } }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/enabled": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", @@ -482,6 +1192,29 @@ "node": ">= 0.8" } }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -491,6 +1224,37 @@ "once": "^1.4.0" } }, + "node_modules/enhanced-resolve": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "license": "MIT", + "optional": true + }, "node_modules/es-define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", @@ -515,6 +1279,15 @@ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -523,17 +1296,27 @@ "node": ">= 0.6" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/express": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", - "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -564,6 +1347,21 @@ "node": ">= 0.10.0" } }, + "node_modules/express-rate-limit": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.4.1.tgz", + "integrity": "sha512-KS3efpnpIDVIXopMc65EMbWbUht7qvTCdtCR2dD/IZmi9MIkopYESwyRqLgv8Pfu589+KqDqOdzJWW7AHoACeg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": "4 || 5 || ^5.0.0-beta.1" + } + }, "node_modules/express/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -579,6 +1377,20 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", + "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/fecha": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", @@ -608,6 +1420,25 @@ "node": "^12.20 || >= 14.13" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/finalhandler": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", @@ -679,12 +1510,39 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "license": "MIT" }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "license": "ISC" }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -693,6 +1551,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^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" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/get-intrinsic": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", @@ -711,6 +1590,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -732,6 +1617,45 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/global-directory": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", + "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "4.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/global-directory/node_modules/ini": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -743,6 +1667,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/has-property-descriptors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", @@ -776,6 +1717,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -787,6 +1734,13 @@ "node": ">= 0.4" } }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "license": "BSD-2-Clause", + "optional": true + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -802,6 +1756,44 @@ "node": ">= 0.8" } }, + "node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -833,6 +1825,50 @@ ], "license": "BSD-3-Clause" }, + "node_modules/ignore": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-6.0.2.tgz", + "integrity": "sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "license": "ISC", + "optional": true + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -850,21 +1886,166 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.10" + "node": ">=10.13.0" + } + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "license": "MIT", + "optional": true, + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ip-address/node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-installed-globally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-1.0.0.tgz", + "integrity": "sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-directory": "^4.0.1", + "is-path-inside": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "license": "MIT", + "optional": true + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", + "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", - "license": "MIT" - }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -877,12 +2058,92 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC", + "optional": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/js-yaml/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "license": "MIT", + "optional": true + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/kuler": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", "license": "MIT" }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "license": "MIT" + }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "license": "MIT" + }, "node_modules/logform": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/logform/-/logform-2.6.1.tgz", @@ -900,6 +2161,71 @@ "node": ">= 12.0.0" } }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "license": "ISC", + "optional": true, + "dependencies": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -908,6 +2234,22 @@ "node": ">= 0.6" } }, + "node_modules/memoize": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/memoize/-/memoize-10.0.0.tgz", + "integrity": "sha512-H6cBLgsi6vMWOcCpvVCdFFnl3kerEXbrYh9q+lY6VXvQSmM6CkmV08VOwT+WE2tzIEqRPFfAq3fm4v/UIW6mSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/memoize?sponsor=1" + } + }, "node_modules/merge-descriptors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", @@ -957,6 +2299,31 @@ "node": ">= 0.6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -969,6 +2336,122 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "encoding": "^0.1.12" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -988,6 +2471,12 @@ "license": "MIT", "optional": true }, + "node_modules/napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", + "license": "MIT" + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -997,6 +2486,24 @@ "node": ">= 0.6" } }, + "node_modules/node-abi": { + "version": "3.71.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.71.0.tgz", + "integrity": "sha512-SZ40vRiy/+wRTf21hxkkEjPJZpARzUMVcJoQse2EF8qkUWbbO2z7vd5oA/H6bVH6SZQ5STGcu0KRDS7biNRfxw==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -1034,6 +2541,102 @@ "url": "https://opencollective.com/node-fetch" } }, + "node_modules/node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "license": "MIT", + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 10.12.0" + } + }, + "node_modules/nodemon": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz", + "integrity": "sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -1083,6 +2686,29 @@ "fn.name": "1.x.x" } }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "license": "MIT", + "peer": true + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -1100,11 +2726,99 @@ "node": ">=0.10.0" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, "node_modules/path-to-regexp": { "version": "0.1.10", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", + "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "license": "ISC", + "optional": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "license": "MIT", + "optional": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -1118,6 +2832,13 @@ "node": ">= 0.10" } }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -1128,6 +2849,15 @@ "once": "^1.3.1" } }, + "node_modules/python-shell": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/python-shell/-/python-shell-5.0.0.tgz", + "integrity": "sha512-RUOOOjHLhgR1MIQrCtnEqz/HJ1RMZBIN+REnpSUrfft2bXqXy69fwJASVziWExfFXsR1bCY0TznnHooNsCo0/w==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, "node_modules/qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", @@ -1164,6 +2894,21 @@ "node": ">= 0.8" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -1178,6 +2923,96 @@ "node": ">= 6" } }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/regexp-tree": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", + "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", + "dev": true, + "license": "MIT", + "bin": { + "regexp-tree": "bin/regexp-tree" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1198,6 +3033,16 @@ ], "license": "MIT" }, + "node_modules/safe-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz", + "integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "regexp-tree": "~0.1.1" + } + }, "node_modules/safe-stable-stringify": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", @@ -1213,6 +3058,18 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/send": { "version": "0.19.0", "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", @@ -1276,6 +3133,12 @@ "node": ">= 0.8.0" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -1297,30 +3160,142 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, - "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "license": "MIT", + "optional": true, "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 10.0.0", + "npm": ">= 3.0.0" } }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "node_modules/socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", "license": "MIT", + "optional": true, "dependencies": { - "is-arrayish": "^0.3.1" + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" } }, "node_modules/split-ca": { @@ -1335,6 +3310,30 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "license": "BSD-3-Clause" }, + "node_modules/sqlite3": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", + "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.1", + "tar": "^6.1.11" + }, + "optionalDependencies": { + "node-gyp": "8.x" + }, + "peerDependencies": { + "node-gyp": "8.x" + }, + "peerDependenciesMeta": { + "node-gyp": { + "optional": true + } + } + }, "node_modules/ssh2": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.15.0.tgz", @@ -1352,6 +3351,19 @@ "nan": "^2.18.0" } }, + "node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", @@ -1378,6 +3390,178 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/swagger-jsdoc": { + "version": "6.2.8", + "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz", + "integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==", + "license": "MIT", + "dependencies": { + "commander": "6.2.0", + "doctrine": "3.0.0", + "glob": "7.1.6", + "lodash.mergewith": "^4.6.2", + "swagger-parser": "^10.0.3", + "yaml": "2.0.0-1" + }, + "bin": { + "swagger-jsdoc": "bin/swagger-jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/swagger-jsdoc/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==", + "license": "MIT", + "dependencies": { + "@apidevtools/swagger-parser": "10.0.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.17.14", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.17.14.tgz", + "integrity": "sha512-CVbSfaLpstV65OnSjbXfVd6Sta3q3F7Cj/yYuvHMp1P90LztOLs6PfUnKEVAeiIVQt9u2SaPwv0LiH/OyMjHRw==", + "license": "Apache-2.0" + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/tar-fs": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz", @@ -1406,12 +3590,50 @@ "node": ">=6" } }, + "node_modules/tar/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/teamcity-service-messages": { + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/teamcity-service-messages/-/teamcity-service-messages-0.1.14.tgz", + "integrity": "sha512-29aQwaHqm8RMX74u2o/h1KbMLP89FjNiMxD9wbF2BbWOnbM+q+d1sCEC+MqCc4QW3NJykn77OMpTFw/xTHIc0w==", + "dev": true, + "license": "MIT" + }, "node_modules/text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", "license": "MIT" }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -1420,6 +3642,22 @@ "node": ">=0.6" } }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/triple-beam": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", @@ -1429,6 +3667,48 @@ "node": ">= 14.0.0" } }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tsconfig-paths-webpack-plugin": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.1.0.tgz", + "integrity": "sha512-xWFISjviPydmtmgeUAuXp4N1fky+VCtfhOkDUFIv5ea7p4wuTomI4QTrXvFBX2S4jZsmyTSrStQl+E+4w+RzxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.7.0", + "tsconfig-paths": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", @@ -1447,6 +3727,33 @@ "node": ">= 0.6" } }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "license": "ISC", + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -1470,6 +3777,15 @@ "node": ">= 0.4.0" } }, + "node_modules/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -1479,6 +3795,19 @@ "node": ">= 0.8" } }, + "node_modules/watskeburt": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/watskeburt/-/watskeburt-4.1.0.tgz", + "integrity": "sha512-KkY5H51ajqy9HYYI+u9SIURcWnqeVVhdH0I+ab6aXPGHfZYxgRCwnR6Lm3+TYB6jJVt5jFqw4GAKmwf1zHmGQw==", + "dev": true, + "license": "MIT", + "bin": { + "watskeburt": "dist/run-cli.js" + }, + "engines": { + "node": "^18||>=20" + } + }, "node_modules/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", @@ -1488,10 +3817,51 @@ "node": ">= 8" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "optional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, "node_modules/winston": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.14.2.tgz", - "integrity": "sha512-CO8cdpBB2yqzEf8v895L+GNKYJiEq8eKlHU38af3snQBQ+sdAIUepjMSguOIJC7ICbzm0ZI+Af2If4vIJrtmOg==", + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.15.0.tgz", + "integrity": "sha512-RhruH2Cj0bV0WgNL+lOfoUBI4DVfdUNjVnJGVovWZmrcKtrFTTRzgXYK2O9cymSGjrERCtaAeHwMNnUWXlwZow==", "license": "MIT", "dependencies": { "@colors/colors": "^1.6.0", @@ -1530,6 +3900,21 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.0.0-1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", + "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/yamljs": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz", @@ -1543,6 +3928,36 @@ "json2yaml": "bin/json2yaml", "yaml2json": "bin/yaml2json" } + }, + "node_modules/z-schema": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", + "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", + "license": "MIT", + "dependencies": { + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + }, + "bin": { + "z-schema": "bin/z-schema" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "commander": "^9.4.1" + } + }, + "node_modules/z-schema/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^12.20.0 || >=14" + } } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 372caad..bd38ba8 100644 --- a/package.json +++ b/package.json @@ -2,21 +2,33 @@ "name": "dockstatapi", "version": "1.0.0", "description": "API for docker hosts using dockerode", - "main": "dockerstatsapi.js", + "main": "server.js", "scripts": { - "start": "node dockstatapi.js", - "test": "echo \"Error: no test specified\" && exit 1" + "start": "node server.js", + "dev": "nodemon server.js", + "offline": "OFFLINE=true nodemon server.js", + "dep": "bash ./utils/createDependencyGraph.sh" }, "keywords": [], "author": "Its4Nik", "license": "ISC", "dependencies": { + "bcrypt": "^5.1.1", "child_process": "^1.0.2", "cors": "^2.8.5", "dockerode": "^4.0.2", - "express": "^4.21.0", + "express": "^4.21.1", + "express-rate-limit": "^7.4.1", "node-fetch": "^3.3.2", - "winston": "^3.14.2", + "python-shell": "^5.0.0", + "sqlite3": "^5.1.7", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1", + "winston": "^3.15.0", "yamljs": "^0.3.0" + }, + "devDependencies": { + "dependency-cruiser": "^16.5.0", + "nodemon": "^3.1.7" } -} \ No newline at end of file +} diff --git a/routes/auth/routes.js b/routes/auth/routes.js new file mode 100644 index 0000000..a0b217e --- /dev/null +++ b/routes/auth/routes.js @@ -0,0 +1,145 @@ +const express = require("express"); +const bcrypt = require("bcrypt"); +const fs = require("fs"); +const path = require("path"); +const logger = require("../../utils/logger"); +const router = express.Router(); +const passwordFile = path.join(__dirname, "../../middleware/password.json"); +const passwordBool = path.join(__dirname, "../../middleware/usePassword.txt"); +const saltRounds = 10; + +function setTrue() { + fs.writeFile(passwordBool, "true", "utf8", (err) => { + if (err) { + logger.error("Error writing to the file:", err); + return; + } + logger.info(`Status "true" has been written to the file.`); + }); +} + +function setFalse() { + fs.writeFile(passwordBool, "false", "utf8", (err) => { + if (err) { + logger.error("Error writing to the file:", err); + return; + } + logger.info(`Status "false" has been written to the file.`); + }); +} + +/** + * @swagger + * /auth/enable: + * post: + * summary: Enable authentication by setting a password + * tags: [Authentication] + * parameters: + * - name: password + * in: query + * required: true + * responses: + * 200: + * description: Authentication enabled. + * 400: + * description: Password is required. + * 500: + * description: Error saving password. + */ +router.post("/enable", (req, res) => { + fs.readFile(passwordBool, "utf8", (err, data) => { + const password = req.query.password; + if (err) { + logger.error("Error reading the file:", err); + return; + } + + const isAuthEnabled = data.trim() === "true"; + if (isAuthEnabled) { + logger.error( + "Passowrd Authentication is already enabled, please dactivate it first", + ); + return res.status(401).json({ + message: + "Passowrd Authentication is already enabled, please dactivate it first", + }); + } + + if (!password) { + return res.status(400).json({ message: "Password is required" }); + } + + bcrypt.genSalt(saltRounds, (err, salt) => { + if (err) { + logger.error("Error generating salt"); + return res.status(500).json({ message: "Error generating salt" }); + } + + bcrypt.hash(password, salt, (err, hash) => { + if (err) { + logger.error("Error hashing password"); + return res.status(500).json({ message: "Error hashing password" }); + } + + const passwordData = { hash, salt }; + fs.writeFile(passwordFile, JSON.stringify(passwordData), (err) => { + if (err) + return res.status(500).json({ message: "Error saving password" }); + setTrue(); + res.json({ message: "Authentication enabled" }); + }); + }); + }); + }); +}); + +/** + * @swagger + * /auth/disable: + * post: + * summary: Disable authentication by providing the existing password + * tags: [Authentication] + * parameters: + * - name: password + * in: query + * required: true + * responses: + * 200: + * description: Authentication disabled. + * 400: + * description: Password is required. + * 401: + * description: Invalid password. + * 500: + * description: Error disabling authentication. + */ +router.post("/disable", (req, res) => { + const password = req.query.password; + if (!password) { + logger.error("Password is required!"); + return res.status(400).json({ message: "Password is required" }); + } + + fs.readFile(passwordFile, "utf8", (err, data) => { + if (err) { + logger.error("Error reading password"); + return res.status(500).json({ message: "Error reading password" }); + } + + const storedData = JSON.parse(data); + bcrypt.compare(password, storedData.hash, (err, result) => { + if (err) { + logger.error("Error validating password"); + return res.status(500).json({ message: "Error validating password" }); + } + if (!result) { + logger.error("Invalid password"); + return res.status(401).json({ message: "Invalid password" }); + } + setFalse(); + res.json({ message: "Authentication disabled" }); + }); + }); +}); + +module.exports = router; diff --git a/routes/data/routes.js b/routes/data/routes.js new file mode 100644 index 0000000..adce8d7 --- /dev/null +++ b/routes/data/routes.js @@ -0,0 +1,111 @@ +const express = require("express"); +const router = express.Router(); +const db = require("../../config/db"); +const logger = require("../../utils/logger"); + +function formatRows(rows) { + return rows.reduce((acc, row, index) => { + acc[index] = JSON.parse(row.info); + return acc; + }, {}); +} + +/** + * @swagger + * /data/latest: + * get: + * summary: Retrieve the latest entry from the database + * tags: [Database queries] + * responses: + * 200: + * description: A JSON object containing the latest entry's 'info' data. + * content: + * application/json: + * schema: + * type: object + * example: + * name: "Container A" + * id: "abcd1234" + * cpu_usage: 30 + * mem_usage: 2048 + */ +router.get("/latest", (req, res) => { + db.get( + "SELECT info FROM data ORDER BY timestamp DESC LIMIT 1", + (err, row) => { + if (err) { + logger.error("Error fetching latest data:", err.message); + return res.status(500).json({ error: "Internal server error" }); + } + res.json(JSON.parse(row.info)); + }, + ); +}); + +/** + * @swagger + * /data/time/24h: + * get: + * summary: Retrieve entries from the last 24 hours from the database + * tags: [Database queries] + * responses: + * 200: + * description: A numbered array of 'info' JSON objects from the last 24 hours. + * content: + * application/json: + * schema: + * type: object + * example: + * 0: + * name: "Container A" + * id: "abcd1234" + * cpu_usage: 30 + * mem_usage: 2048 + * 1: + * name: "Container B" + * id: "efgh5678" + * cpu_usage: 45 + * mem_usage: 3072 + */ +router.get("/time/24h", (req, res) => { + const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); + db.all( + "SELECT info FROM data WHERE timestamp >= ?", + [oneDayAgo], + (err, rows) => { + if (err) { + logger.error("Error fetching data from last 24 hours:", err.message); + return res.status(500).json({ error: "Internal server error" }); + } + res.json(formatRows(rows)); + }, + ); +}); + +/** + * @swagger + * /data/clear: + * delete: + * summary: Clear all entries from the database + * tags: [Database queries] + * responses: + * 200: + * description: A message indicating whether the database was cleared successfully. + * content: + * application/json: + * schema: + * type: object + * example: + * message: "Database cleared successfully." + */ +router.delete("/clear", (req, res) => { + db.run("DELETE FROM data", (err) => { + if (err) { + logger.error("Error clearing the database:", err.message); + return res.status(500).json({ error: "Internal server error" }); + } + res.json({ message: "Database cleared successfully" }); + }); +}); + +module.exports = router; diff --git a/routes/frontendController/routes.js b/routes/frontendController/routes.js new file mode 100644 index 0000000..986276f --- /dev/null +++ b/routes/frontendController/routes.js @@ -0,0 +1,340 @@ +const express = require("express"); +const router = express.Router(); +const logger = require("../../utils/logger"); +const { + hideContainer, + unhideContainer, + addTagToContainer, + removeTagFromContainer, + pinContainer, + unpinContainer, +} = require("../../controllers/frontendConfiguration"); + +/** + * @swagger + * /frontend/hide/{containerName}: + * post: + * summary: Hide a container + * tags: [Frontend Configuration] + * parameters: + * - in: path + * name: containerName + * schema: + * type: string + * required: true + * description: The name of the container to hide + * responses: + * 200: + * description: Container hidden successfully. + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates if the operation was successful + * message: + * type: string + * description: Success message + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates if the operation was successful + * error: + * type: string + * description: Error message + */ +// Hide a container +router.post("/hide/:containerName", async (req, res) => { + const { containerName } = req.params; + const target = containerName; + //console.log(target); + + try { + await hideContainer(target); + res.json({ success: true, message: `Container, ${target}, hidden.` }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +/** + * @swagger + * /frontend/unhide/{containerName}: + * post: + * summary: Unhide a container + * tags: [Frontend Configuration] + * parameters: + * - in: path + * name: containerName + * schema: + * type: string + * required: true + * description: The name of the container to unhide + * responses: + * 200: + * description: Container unhidden successfully. + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates if the operation was successful + * message: + * type: string + * description: Success message + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates if the operation was successful + * error: + * type: string + * description: Error message + */ +// Unhide a container +router.post("/unhide/:containerName", async (req, res) => { + const { containerName } = req.params; + try { + await unhideContainer(containerName); + res.json({ success: true, message: "Container unhidden successfully." }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +/** + * @swagger + * /frontend/tag/{containerName}/{tag}: + * post: + * summary: Add a tag to a container + * tags: [Frontend Configuration] + * parameters: + * - in: path + * name: containerName + * schema: + * type: string + * required: true + * description: The name of the container to add tag to + * - in: path + * name: tag + * schema: + * type: string + * required: true + * description: The tag to add + * responses: + * 200: + * description: Tag added successfully. + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates if the operation was successful + * message: + * type: string + * description: Success message + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates if the operation was successful + * error: + * type: string + * description: Error message + */ +// Add a tag to a container +router.post("/tag/:containerName/:tag", async (req, res) => { + const { containerName, tag } = req.params; + try { + await addTagToContainer(containerName, tag); + res.json({ success: true, message: "Tag added successfully." }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +/** + * @swagger + * /frontend/remove-tag/{containerName}/{tag}: + * post: + * summary: Remove a tag from a container + * tags: [Frontend Configuration] + * parameters: + * - in: path + * name: containerName + * schema: + * type: string + * required: true + * description: The name of the container to remove tag from + * - in: path + * name: tag + * schema: + * type: string + * required: true + * description: The tag to remove + * responses: + * 200: + * description: Tag removed successfully. + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates if the operation was successful + * message: + * type: string + * description: Success message + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates if the operation was successful + * error: + * type: string + * description: Error message + */ +// Remove a tag from a container +router.post("/remove-tag/:containerName/:tag", async (req, res) => { + const { containerName, tag } = req.params; + try { + await removeTagFromContainer(containerName, tag); + res.json({ success: true, message: "Tag removed successfully." }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +/** + * @swagger + * /frontend/pin/{containerName}: + * post: + * summary: Pin a container + * tags: [Frontend Configuration] + * parameters: + * - in: path + * name: containerName + * schema: + * type: string + * required: true + * description: The name of the container to pin + * responses: + * 200: + * description: Container pinned successfully. + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates if the operation was successful + * message: + * type: string + * description: Success message + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates if the operation was successful + * error: + * type: string + * description: Error message + */ +// Pin a container +router.post("/pin/:containerName", async (req, res) => { + const { containerName } = req.params; + try { + await pinContainer(containerName); + res.json({ success: true, message: "Container pinned successfully." }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +/** + * @swagger + * /frontend/unpin/{containerName}: + * post: + * summary: Unpin a container + * tags: [Frontend Configuration] + * parameters: + * - in: path + * name: containerName + * schema: + * type: string + * required: true + * description: The name of the container to unpin + * responses: + * 200: + * description: Container unpinned successfully. + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates if the operation was successful + * message: + * type: string + * description: Success message + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates if the operation was successful + * error: + * type: string + * description: Error message + */ +// Unpin a container +router.post("/unpin/:containerName", async (req, res) => { + const { containerName } = req.params; + try { + await unpinContainer(containerName); + res.json({ success: true, message: "Container unpinned successfully." }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +module.exports = router; diff --git a/routes/getter/routes.js b/routes/getter/routes.js new file mode 100644 index 0000000..6c5fbc0 --- /dev/null +++ b/routes/getter/routes.js @@ -0,0 +1,334 @@ +const extractRelevantData = require("../../utils/extractHostData"); +const express = require("express"); +const router = express.Router(); +const { + writeOfflineLog, + readOfflineLog, +} = require("../../utils/writeOfflineLog"); +const { getDockerClient } = require("../../utils/dockerClient"); +const { fetchAllContainers } = require("../../utils/containerService"); +const { getCurrentSchedule } = require("../../controllers/scheduler"); +const logger = require("../../utils/logger"); +const path = require("path"); +const fs = require("fs"); + +/** + * @swagger + * /api/hosts: + * get: + * summary: Retrieve a list of all available Docker hosts + * tags: [Hosts] + * responses: + * 200: + * description: A JSON object containing an array of host names. + * content: + * application/json: + * schema: + * type: object + * properties: + * hosts: + * type: array + * items: + * type: string + * example: ["local", "remote1"] + */ + +router.get("/hosts", (req, res) => { + const config = require("../../config/dockerConfig.json"); + const hosts = config.hosts.map((host) => host.name); + logger.info("Fetching all available Docker hosts"); + res.status(200).json({ hosts }); +}); + +/** + * @swagger + * /api/host/{hostName}/stats: + * get: + * summary: Retrieve statistics for a specified Docker host + * tags: [Hosts] + * parameters: + * - name: hostName + * in: path + * required: true + * description: The name of the host for which to fetch statistics. + * schema: + * type: string + * responses: + * 200: + * description: A JSON object containing relevant statistics for the specified host. + * content: + * application/json: + * schema: + * type: object + * properties: + * hostName: + * type: string + * description: The name of the Docker host. + * info: + * type: object + * description: Information about the Docker host (e.g., storage, running containers). + * version: + * type: object + * description: Version details of the Docker installation on the host. + * 500: + * description: An error occurred while fetching host statistics. + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * description: Error message detailing the issue encountered. + */ +router.get("/host/:hostName/stats", async (req, res) => { + const hostName = req.params.hostName; + logger.info(`Fetching stats for host: ${hostName}`); + if (process.env.OFFLINE === "true") { + logger.info("Fetching offline Host Stats"); + res.status(200).json(readOfflineLog); + } else { + try { + const docker = getDockerClient(hostName); + const info = await docker.info(); + const version = await docker.version(); + const relevantData = extractRelevantData({ hostName, info, version }); + + writeOfflineLog(JSON.stringify(relevantData)); + res.status(200).json(relevantData); + } catch (error) { + logger.error( + `Error fetching stats for host: ${hostName} - ${error.message || "Unknown error"}`, + ); + res.status(500).json({ + error: `Error fetching host stats: ${error.message || "Unknown error"}`, + }); + } + } +}); + +/** + * @swagger + * /api/containers: + * get: + * summary: Retrieve all Docker containers across all configured hosts + * tags: [Containers] + * responses: + * 200: + * description: A JSON object containing container data for all hosts. + * content: + * application/json: + * schema: + * type: object + * additionalProperties: + * type: object + * properties: + * name: + * type: string + * description: Name of the container. + * id: + * type: string + * description: Unique identifier for the container. + * hostName: + * type: string + * description: The host on which the container is running. + * state: + * type: string + * description: Current state of the container (e.g., running, exited). + * cpu_usage: + * type: number + * format: double + * description: CPU usage in nanoseconds. + * mem_usage: + * type: number + * description: Memory usage in bytes. + * mem_limit: + * type: number + * description: Memory limit in bytes. + * net_rx: + * type: number + * description: Total received bytes over the network. + * net_tx: + * type: number + * description: Total transmitted bytes over the network. + * current_net_rx: + * type: number + * description: Current received bytes over the network. + * current_net_tx: + * type: number + * description: Current transmitted bytes over the network. + * networkMode: + * type: string + * description: Network mode configured for the container. + * link: + * type: string + * description: Optional link to additional information. + * icon: + * type: string + * description: Optional icon representing the container. + * tags: + * type: string + * description: Optional tags associated with the container. + * 500: + * description: An error occurred while fetching container data. + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * description: Error message detailing the issue encountered. + */ +router.get("/containers", async (req, res) => { + logger.info("Fetching all containers across all hosts"); + try { + const allContainerData = await fetchAllContainers(); + res.status(200).json(allContainerData); + } catch (error) { + logger.error(`Error fetching containers: ${error.message}`); + res.status(500).json({ error: "Failed to fetch containers" }); + } +}); + +/** + * @swagger + * /api/config: + * get: + * summary: Retrieve Docker configuration + * tags: [Configuration] + * responses: + * 200: + * description: A JSON object containing the Docker configuration. + * content: + * application/json: + * schema: + * type: object + * additionalProperties: true + * 500: + * description: An error occurred while loading the Docker configuration. + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * description: Error message detailing the issue encountered. + */ +router.get("/config", async (req, res) => { + const configPath = path.join(__dirname, "../../config/dockerConfig.json"); + try { + const rawData = fs.readFileSync(configPath); + const jsonData = JSON.parse(rawData.toString()); + res.status(200).json(jsonData); + } catch (error) { + logger.error("Error loading dockerConfig.json: " + error.message); + res.status(500).json({ error: "Failed to load Docker configuration" }); + } +}); + +/** + * @swagger + * /api/current-schedule: + * get: + * summary: Get the current fetch schedule in seconds + * tags: [Configuration] + * responses: + * 200: + * description: Current fetch schedule retrieved successfully. + * content: + * application/json: + * schema: + * type: object + * properties: + * interval: + * type: integer + * description: Current fetch interval in seconds. + */ +router.get("/current-schedule", (req, res) => { + const currentSchedule = getCurrentSchedule(); + res.json(currentSchedule); +}); + +/** + * @swagger + * /api/status: + * get: + * summary: Check server status + * tags: [Misc] + * description: Returns a 200 status with an "up" message to indicate the server is up and running. Used for Healthchecks + * responses: + * 200: + * description: Server is running + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: "up" + */ +router.get("/status", (req, res) => { + res.status(200).json({ status: "up" }); +}); + +/** + * @swagger + * /api/frontend-config: + * get: + * summary: Get Frontend Configuration + * tags: [Configuration] + * description: Retrieves the frontend configuration data. + * responses: + * 200: + * description: Success + * content: + * application/json: + * schema: + * type: array + * items: + * type: object + * properties: + * name: + * type: string + * description: Container Name + * hidden: + * type: boolean + * description: Whether the container is hidden + * tags: + * type: array + * items: + * type: string + * description: Tags associated with the container + * pinned: + * type: boolean + * description: Whether the container is pinned + * 500: + * description: Internal Server Error + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * description: Error message + */ +router.get("/frontend-config", (req, res) => { + const configPath = path.join( + __dirname, + "../../data/frontendConfiguration.json", + ); + try { + const rawData = fs.readFileSync(configPath); + const jsonData = JSON.parse(rawData.toString()); + res.status(200).json(jsonData); + } catch (error) { + logger.error("Error loading frontendConfiguration.json: " + error.message); + res.status(500).json({ error: "Failed to load Frontend configuration" }); + } +}); + +module.exports = router; diff --git a/routes/setter/routes.js b/routes/setter/routes.js new file mode 100644 index 0000000..24ae2ad --- /dev/null +++ b/routes/setter/routes.js @@ -0,0 +1,145 @@ +const { + setFetchInterval, + parseInterval, +} = require("../../controllers/scheduler"); +const express = require("express"); +const router = express.Router(); +const path = require("path"); +const fs = require("fs"); +const logger = require("../../utils/logger"); + +/** + * @swagger + * /conf/addHost: + * put: + * summary: Add a new host to the Docker configuration + * tags: [Configuration] + * parameters: + * - name: name + * in: query + * required: true + * description: The name of the new host. + * - name: url + * in: query + * required: true + * description: The URL of the new host. + * - name: port + * in: query + * required: true + * description: The port of the new host. + * responses: + * 200: + * description: Host added successfully. + * 400: + * description: Bad request, invalid input. + * 500: + * description: An error occurred while adding the host. + */ +router.put("/addHost", async (req, res) => { + const name = req.query.name; + const url = req.query.url; + const port = req.query.port; + const configPath = path.join(__dirname, "../../config/dockerConfig.json"); + + if (!name || !url || !port) { + return res.status(400).json({ error: "Name, Port and URL are required." }); + } + + try { + const rawData = fs.readFileSync(configPath); + const config = JSON.parse(rawData); + + // Check for existing host + if (config.hosts.some((host) => host.name === name)) { + return res.status(400).json({ error: "Host already exists." }); + } + + config.hosts.push({ name, url, port }); + fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); + logger.info(`Added new host: ${name}`); + res.status(200).json({ message: "Host added successfully." }); + } catch (error) { + logger.error("Error adding host: " + error.message); + res.status(500).json({ error: "Failed to add host." }); + } +}); + +/** + * @swagger + * /conf/scheduler: + * put: + * summary: Set fetch interval for data fetching + * tags: [Configuration] + * parameters: + * - name: interval + * in: query + * required: true + * description: The new interval for fetching data, e.g., "6h 20m", "300s". + * responses: + * 200: + * description: Fetch interval set successfully. + * 400: + * description: Invalid interval format or out of range. + */ +router.put("/scheduler", (req, res) => { + const interval = req.query.interval; + const newInterval = parseInterval(interval); + + if (newInterval < 5 * 60 * 1000 || newInterval > 6 * 60 * 60 * 1000) { + return res + .status(400) + .json({ error: "Interval must be between 5 minutes and 6 hours." }); + } + + setFetchInterval(newInterval); + res.json({ message: `Fetch interval set to ${interval}.` }); +}); + +/** + * @swagger + * /conf/removeHost: + * delete: + * summary: Remove a host from the Docker configuration + * tags: [Configuration] + * parameters: + * - name: hostName + * in: query + * required: true + * description: The name of the host to remove. + * responses: + * 200: + * description: Host removed successfully. + * 404: + * description: Host not found. + * 500: + * description: An error occurred while removing the host. + */ +router.delete("/removeHost", async (req, res) => { + const hostName = req.query.hostName; + const configPath = path.join(__dirname, "../../config/dockerConfig.json"); + + if (!hostName) { + return res.status(400).json({ error: "Host name is required." }); + } + + try { + const rawData = fs.readFileSync(configPath); + const config = JSON.parse(rawData); + + // Check for existing host + const hostIndex = config.hosts.findIndex((host) => host.name === hostName); + if (hostIndex === -1) { + return res.status(404).json({ error: "Host not found." }); + } + + config.hosts.splice(hostIndex, 1); + fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); + logger.info(`Removed host: ${hostName}`); + res.status(200).json({ message: "Host removed successfully." }); + } catch (error) { + logger.error("Error removing host: " + error.message); + res.status(500).json({ error: "Failed to remove host." }); + } +}); + +module.exports = router; diff --git a/scripts/install_apprise.sh b/scripts/install_apprise.sh deleted file mode 100644 index 7506d0e..0000000 --- a/scripts/install_apprise.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -VENV_DIR="/api" - -apk update -apk add python3 py3-pip py3-virtualenv - -python3 -m venv "$VENV_DIR" - -. "$VENV_DIR/bin/activate" - -pip install apprise - -deactivate - -echo "Apprise has been successfully installed in the virtual environment." diff --git a/scripts/notify.sh b/scripts/notify.sh deleted file mode 100755 index 54dc226..0000000 --- a/scripts/notify.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/bash - -NOTIFY_TYPE=$1 # ADD, REMOVE, EXIT, ANY -CONTAINER_ID=$2 # Container ID -CONTAINER_NAME=$3 # Container Name -HOST=$4 # Host Name -STATE=$5 # Current State - -ADD_MESSAGE="${ADD_MESSAGE:-🆕 Container Added: $CONTAINER_NAME ($CONTAINER_ID) on $HOST}" -REMOVE_MESSAGE="${REMOVE_MESSAGE:-🚫 Container Removed: $CONTAINER_NAME ($CONTAINER_ID) on $HOST}" -EXIT_MESSAGE="${EXIT_MESSAGE:-❌ Container Exited: $CONTAINER_NAME ($CONTAINER_ID) on $HOST}" -ANY_MESSAGE="${ANY_MESSAGE:-⚠️ Container State Changed: $CONTAINER_NAME ($CONTAINER_ID) on $HOST - New State: $STATE}" - -case "$NOTIFY_TYPE" in - ADD) - MESSAGE="$ADD_MESSAGE" - ;; - REMOVE) - MESSAGE="$REMOVE_MESSAGE" - ;; - EXIT) - MESSAGE="$EXIT_MESSAGE" - ;; - ANY) - MESSAGE="$ANY_MESSAGE" - ;; - *) - MESSAGE="Unknown action for $CONTAINER_NAME ($CONTAINER_ID) on $HOST" - ;; -esac - -if [[ ! -f ./config/apprise_config.yml ]]; then - echo -n "No Apprise configuration found, aborting." - exit 1 -fi - -# Send notification via Apprise - -### PYTHON ENVIRONMENT: ### -. /api/bin/activate - -apprise -b "$MESSAGE" --config ./config/apprise_config.yml - -deactivate -########################### - -exit 0 diff --git a/server.js b/server.js new file mode 100644 index 0000000..b7a2731 --- /dev/null +++ b/server.js @@ -0,0 +1,33 @@ +const express = require("express"); +const swaggerDocs = require("./swagger/swaggerDocs"); +const api = require("./routes/getter/routes"); +const conf = require("./routes/setter/routes"); +const auth = require("./routes/auth/routes"); +const data = require("./routes/data/routes"); +const frontend = require("./routes/frontendController/routes"); +const authMiddleware = require("./middleware/authMiddleware"); +const app = express(); +const logger = require("./utils/logger"); +const { scheduleFetch } = require("./controllers/scheduler"); +const { limiter } = require("./utils/rateLimiter"); + +const PORT = "7070"; + +app.use(express.json()); + +app.use("/api-docs", (req, res, next) => next()); + +swaggerDocs(app); +scheduleFetch(); + +// Routes +app.use("/api", authMiddleware, api); +app.use("/conf", authMiddleware, conf); +app.use("/auth", authMiddleware, auth); +app.use("/data", authMiddleware, data); +app.use("/frontend", authMiddleware, frontend); + +app.listen(PORT, () => { + logger.info(`Server is running on http://localhost:${PORT}`); + logger.info(`Swagger docs available at http://localhost:${PORT}/api-docs`); +}); diff --git a/swagger/swaggerDocs.js b/swagger/swaggerDocs.js new file mode 100644 index 0000000..5719372 --- /dev/null +++ b/swagger/swaggerDocs.js @@ -0,0 +1,10 @@ +const swaggerUi = require("swagger-ui-express"); +const swaggerJsdoc = require("swagger-jsdoc"); +const swaggerConfig = require("../config/swaggerConfig"); + +const swaggerDocs = (app) => { + const specs = swaggerJsdoc(swaggerConfig); + app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(specs)); +}; + +module.exports = swaggerDocs; diff --git a/utils/containerService.js b/utils/containerService.js new file mode 100644 index 0000000..eb078a5 --- /dev/null +++ b/utils/containerService.js @@ -0,0 +1,63 @@ +const config = require("../config/dockerConfig.json"); +const logger = require("./logger"); +const { getDockerClient } = require("./dockerClient"); + +async function fetchAllContainers() { + const allContainerData = {}; + + for (const hostConfig of config.hosts) { + const hostName = hostConfig.name; + try { + const docker = getDockerClient(hostName); + const containers = await docker.listContainers({ all: true }); + + allContainerData[hostName] = await Promise.all( + containers.map(async (container) => { + const containerInfo = await docker + .getContainer(container.Id) + .inspect(); + const containerStats = await docker + .getContainer(container.Id) + .stats({ stream: false }); + const cpuDelta = + containerStats.cpu_stats.cpu_usage.total_usage - + containerStats.precpu_stats.cpu_usage.total_usage; + const systemCpuDelta = + containerStats.cpu_stats.system_cpu_usage - + containerStats.precpu_stats.system_cpu_usage; + const cpuUsage = + systemCpuDelta > 0 + ? (cpuDelta / systemCpuDelta) * + containerStats.cpu_stats.online_cpus + : 0; + + return { + name: container.Names[0].replace("/", ""), + id: container.Id, + hostName: hostName, + state: container.State, + cpu_usage: cpuUsage * 1000000000, + mem_usage: containerStats.memory_stats.usage, + mem_limit: containerStats.memory_stats.limit, + net_rx: containerStats.networks?.eth0?.rx_bytes || 0, + net_tx: containerStats.networks?.eth0?.tx_bytes || 0, + current_net_rx: containerStats.networks?.eth0?.rx_bytes || 0, + current_net_tx: containerStats.networks?.eth0?.tx_bytes || 0, + networkMode: containerInfo.HostConfig.NetworkMode, + }; + }), + ); + } catch (error) { + logger.error( + `Error fetching containers for host: ${hostName} - ${error.message}`, + ); + allContainerData[hostName] = { + error: `Error fetching containers: ${error.message}`, + }; + } + } + + return allContainerData; +} + +module.exports = { fetchAllContainers }; diff --git a/utils/createDependencyGraph.sh b/utils/createDependencyGraph.sh new file mode 100755 index 0000000..3e75de0 --- /dev/null +++ b/utils/createDependencyGraph.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +TMP=$(mktemp) + +cat ./server.js | grep "./routes" | awk '{print $2,$4}' > $TMP + +while read line; do + target_route=$(echo "$line" | cut -d '"' -f2) + route=$(echo "$line" | awk '{print $1}') + + echo + echo "Route: $route" + echo ${target_route}.js + + + npx depcruise \ + -p cli-feedback \ + -T mermaid \ + -x "^node_modules|logger|.dependency-cruiser|path|fs" \ + -f ./misc/dependencyGraphs/mermaid-${route}.txt \ + ${target_route}.js + +done < <(cat $TMP) + +npx depcruise \ + -p cli-feedback \ + -T mermaid \ + -x "^node_modules|logger|.dependency-cruiser|path|fs" \ + -f ./misc/dependencyGraphs/mermaid-all.txt \ + ./ + +sleep 0.5 + +echo -e "\n========\n\n DONE\n\n========" diff --git a/utils/dockerClient.js b/utils/dockerClient.js new file mode 100644 index 0000000..3d691e5 --- /dev/null +++ b/utils/dockerClient.js @@ -0,0 +1,45 @@ +const Docker = require("dockerode"); +const fs = require("fs"); +const path = require("path"); +const logger = require("./logger"); + +// Function to dynamically load config on each request +function loadDockerConfig() { + const configPath = path.join(__dirname, "../config/dockerConfig.json"); + try { + const rawData = fs.readFileSync(configPath); + logger.debug("Refreshed DockerConfig.json"); + return JSON.parse(rawData); + } catch (error) { + logger.error("Error loading dockerConfig.json: " + error.message); + throw new Error("Failed to load Docker configuration"); + } +} + +// Function to create the Docker client using separate url and port +function createDockerClient(hostConfig) { + logger.info( + `Creating Docker client for host: ${hostConfig.url} on port: ${hostConfig.port}`, + ); + return new Docker({ + host: hostConfig.url, + port: hostConfig.port || 2375, // Use 2375 as default port for non-TLS + protocol: "http", // Ensure the use of http for non-TLS + }); +} + +// This function will get the Docker client based on the host configuration +const getDockerClient = (hostName) => { + logger.debug(`Getting Docker Client for ${hostName}`); + const config = loadDockerConfig(); // Dynamically load config + const hostConfig = config.hosts.find((host) => host.name === hostName); + + if (!hostConfig) { + const errorMsg = `Docker host ${hostName} not found in configuration`; + logger.error(errorMsg); + throw new Error(errorMsg); + } + return createDockerClient(hostConfig); +}; + +module.exports = { getDockerClient }; diff --git a/utils/extractHostData.js b/utils/extractHostData.js new file mode 100644 index 0000000..87db239 --- /dev/null +++ b/utils/extractHostData.js @@ -0,0 +1,26 @@ +function extractRelevantData(jsonData) { + return { + hostName: jsonData.hostName, + info: { + ID: jsonData.info.ID, + Containers: jsonData.info.Containers, + ContainersRunning: jsonData.info.ContainersRunning, + ContainersPaused: jsonData.info.ContainersPaused, + ContainersStopped: jsonData.info.ContainersStopped, + Images: jsonData.info.Images, + OperatingSystem: jsonData.info.OperatingSystem, + KernelVersion: jsonData.info.KernelVersion, + Architecture: jsonData.info.Architecture, + MemTotal: jsonData.info.MemTotal, + NCPU: jsonData.info.NCPU, + }, + version: { + Components: jsonData.version.Components.reduce((acc, component) => { + acc[component.Name] = component.Version; + return acc; + }, {}), + }, + }; +} + +module.exports = extractRelevantData; diff --git a/utils/logger.js b/utils/logger.js new file mode 100644 index 0000000..853ca6f --- /dev/null +++ b/utils/logger.js @@ -0,0 +1,20 @@ +const winston = require("winston"); +const loggerConfig = require("../config/loggerConfig"); + +const transports = [new winston.transports.Console()]; + +if (loggerConfig.transports.file.enabled) { + transports.push( + new winston.transports.File({ + filename: loggerConfig.transports.file.filename, + }), + ); +} + +const logger = winston.createLogger({ + level: loggerConfig.level, + format: loggerConfig.format, + transports, +}); + +module.exports = logger; diff --git a/utils/rateLimiter.js b/utils/rateLimiter.js new file mode 100644 index 0000000..c323c58 --- /dev/null +++ b/utils/rateLimiter.js @@ -0,0 +1,8 @@ +import { rateLimit } from "express-rate-limit"; + +export const limiter = rateLimit({ + windowMs: 5 * 60 * 1000, // 5 minutes + limit: 300, // Limit each IP to 300 requests per `window` (here, per 5 minutes) + standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers + legacyHeaders: false, // Disable the `X-RateLimit-*` headers +}); diff --git a/utils/writeOfflineLog.js b/utils/writeOfflineLog.js new file mode 100644 index 0000000..4d26b1d --- /dev/null +++ b/utils/writeOfflineLog.js @@ -0,0 +1,31 @@ +const fs = require("fs"); +const path = require("path"); +const logger = require("../utils/logger"); + +const LOG_FILE_PATH = path.join(__dirname, "../logs/hostStats.json"); + +function writeOfflineLog(message) { + try { + if (!fs.existsSync(LOG_FILE_PATH)) { + fs.writeFileSync(LOG_FILE_PATH, message); + } + } catch (error) { + logger.error("Error writing one time reference log: ", error); + } +} + +function readOfflineLog() { + fs.readFile(LOG_FILE_PATH, "utf-8", (err, data) => { + if (err) { + logger.error("Error reading offline log:", err); + } + + logger.debug("Returning data:", data); + return data; + }); +} + +module.exports = { + writeOfflineLog, + readOfflineLog, +}; From e2f3a9364d519b5ec075e6a94102acddfbb831cb Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 1 Nov 2024 19:20:20 +0100 Subject: [PATCH 002/324] Update build-dev.yaml --- .github/workflows/build-dev.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-dev.yaml b/.github/workflows/build-dev.yaml index a8d55f2..d3b1335 100644 --- a/.github/workflows/build-dev.yaml +++ b/.github/workflows/build-dev.yaml @@ -34,7 +34,7 @@ jobs: tags: | type=sha,format=long,prefix= flavor: | - type=schedule,pattern=nightly + type=pep440,pattern={{version}},value=nightly - name: Build and push uses: docker/build-push-action@v5 From 14c0951954286eb981cb4d5a25deba235542215f Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 1 Nov 2024 19:21:42 +0100 Subject: [PATCH 003/324] Update build-dev.yaml --- .github/workflows/build-dev.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/build-dev.yaml b/.github/workflows/build-dev.yaml index d3b1335..cce888e 100644 --- a/.github/workflows/build-dev.yaml +++ b/.github/workflows/build-dev.yaml @@ -32,8 +32,6 @@ jobs: with: images: ghcr.io/${{ github.repository }} tags: | - type=sha,format=long,prefix= - flavor: | type=pep440,pattern={{version}},value=nightly - name: Build and push From f4d8ec10e4a045380da9dc9b51e21fbf97466d7a Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 1 Nov 2024 19:25:41 +0100 Subject: [PATCH 004/324] Update build-dev.yaml --- .github/workflows/build-dev.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-dev.yaml b/.github/workflows/build-dev.yaml index cce888e..9833ede 100644 --- a/.github/workflows/build-dev.yaml +++ b/.github/workflows/build-dev.yaml @@ -32,7 +32,9 @@ jobs: with: images: ghcr.io/${{ github.repository }} tags: | - type=pep440,pattern={{version}},value=nightly + type=raw,enable=true,priority=200,prefix=,suffix=,value=nightly + flavor: | + latest=false - name: Build and push uses: docker/build-push-action@v5 From 5d6c61c52af3e67fc5f9bc6c12ec3bed41eb029f Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 1 Nov 2024 19:29:30 +0100 Subject: [PATCH 005/324] Add rate limiter --- server.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/server.js b/server.js index b7a2731..9ccb80d 100644 --- a/server.js +++ b/server.js @@ -21,11 +21,11 @@ swaggerDocs(app); scheduleFetch(); // Routes -app.use("/api", authMiddleware, api); -app.use("/conf", authMiddleware, conf); -app.use("/auth", authMiddleware, auth); -app.use("/data", authMiddleware, data); -app.use("/frontend", authMiddleware, frontend); +app.use("/api", authMiddleware, limiter, api); +app.use("/conf", authMiddleware, limiter, conf); +app.use("/auth", authMiddleware, limiter, auth); +app.use("/data", authMiddleware, limiter, data); +app.use("/frontend", authMiddleware, limiter, frontend); app.listen(PORT, () => { logger.info(`Server is running on http://localhost:${PORT}`); From 68bb589f6756a06081b05014e38aae0c830852df Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Fri, 1 Nov 2024 20:13:12 +0100 Subject: [PATCH 006/324] Move rate limiter to middleware --- {utils => middleware}/rateLimiter.js | 0 server.js | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename {utils => middleware}/rateLimiter.js (100%) diff --git a/utils/rateLimiter.js b/middleware/rateLimiter.js similarity index 100% rename from utils/rateLimiter.js rename to middleware/rateLimiter.js diff --git a/server.js b/server.js index 9ccb80d..f6a86f3 100644 --- a/server.js +++ b/server.js @@ -9,7 +9,7 @@ const authMiddleware = require("./middleware/authMiddleware"); const app = express(); const logger = require("./utils/logger"); const { scheduleFetch } = require("./controllers/scheduler"); -const { limiter } = require("./utils/rateLimiter"); +const { limiter } = require("./middleware/rateLimiter"); const PORT = "7070"; From 6123267a33b03976661090374e83399592e9f945 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Fri, 1 Nov 2024 21:02:36 +0100 Subject: [PATCH 007/324] Formating and details on swagger page --- config/db.js | 38 +++++++------- config/loggerConfig.js | 35 +++++++------ config/swaggerConfig.js | 57 ++++++++++----------- data/database.db | Bin 16384 -> 126976 bytes misc/dependencyGraphs/mermaid-all.txt | 70 +++++++++++++------------- package.json | 4 +- 6 files changed, 104 insertions(+), 100 deletions(-) diff --git a/config/db.js b/config/db.js index 9317ab4..51850d3 100644 --- a/config/db.js +++ b/config/db.js @@ -1,19 +1,19 @@ -const sqlite3 = require('sqlite3').verbose(); -const logger = require('./../utils/logger'); -const path = require('path'); -const dbPath = path.join(__dirname, '../data/database.db'); - -const db = new sqlite3.Database(dbPath, (err) => { - if (err) { - logger.error('Error opening database:', err.message); - } else { - db.run(`CREATE TABLE IF NOT EXISTS data ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - info TEXT NOT NULL, - timestamp DATETIME DEFAULT CURRENT_TIMESTAMP - )`); - logger.info('Database created / opened succesfully'); - } -}); - -module.exports = db; \ No newline at end of file +const sqlite3 = require("sqlite3").verbose(); +const logger = require("./../utils/logger"); +const path = require("path"); +const dbPath = path.join(__dirname, "../data/database.db"); + +const db = new sqlite3.Database(dbPath, (err) => { + if (err) { + logger.error("Error opening database:", err.message); + } else { + db.run(`CREATE TABLE IF NOT EXISTS data ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + info TEXT NOT NULL, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP + )`); + logger.info("Database created / opened succesfully"); + } +}); + +module.exports = db; diff --git a/config/loggerConfig.js b/config/loggerConfig.js index 7950348..0f7641a 100644 --- a/config/loggerConfig.js +++ b/config/loggerConfig.js @@ -1,16 +1,19 @@ -const { format } = require('winston'); - -module.exports = { - level: 'info', - format: format.combine( - format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), - format.printf(({ timestamp, level, message }) => `${timestamp} [${level.toUpperCase()}]: ${message}`) - ), - transports: { - console: true, - file: { - enabled: true, - filename: 'logs/app.log', - }, - }, -}; +const { format } = require("winston"); + +module.exports = { + level: "info", + format: format.combine( + format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), + format.printf( + ({ timestamp, level, message }) => + `${timestamp} [${level.toUpperCase()}]: ${message}`, + ), + ), + transports: { + console: true, + file: { + enabled: true, + filename: "logs/app.log", + }, + }, +}; diff --git a/config/swaggerConfig.js b/config/swaggerConfig.js index c79ae47..723897f 100644 --- a/config/swaggerConfig.js +++ b/config/swaggerConfig.js @@ -1,28 +1,29 @@ -const options = { - definition: { - openapi: '3.0.0', - info: { - title: 'Your API Documentation', - version: '1.0.0', - description: 'API documentation with authentication', - }, - components: { - securitySchemes: { - passwordAuth: { - type: 'apiKey', - in: 'header', - name: 'x-password', - description: 'Password required for authentication', - }, - }, - }, - security: [ - { - passwordAuth: [], - }, - ], - }, - apis: ['./routes/*/*.js'], // Point to your route files -}; - -module.exports = options; +const options = { + definition: { + failOnErrors: true, + openapi: "3.0.0", + info: { + title: "DockStatAPI", + version: "2", + description: "An API used to query muliple docker hosts", + }, + components: { + securitySchemes: { + passwordAuth: { + type: "apiKey", + in: "header", + name: "x-password", + description: "Password required for authentication", + }, + }, + }, + security: [ + { + passwordAuth: [], + }, + ], + }, + apis: ["./routes/*/*.js"], +}; + +module.exports = options; diff --git a/data/database.db b/data/database.db index 80295980fc1caba1cff16d304197ec55893652f2..619beed4fb9bfcf112978dac7d78da14ad6617c9 100644 GIT binary patch literal 126976 zcmeI5&yyTScHalw5Qa>gC4t5IF>T zfUe9T<~uJlU%v16z4u4|?)y)!yZh_QtEbiV{q*8jF7Dj9_}%;WFD@?b@%y{{9zO2y zhc6F*;j{ex*vGw#)zyFg7Ng$y_W!-O_q)8rkBC4-AR-VEhzLXkA_5VCh(JUjA`lUX z2>j9!_`w@r`OfeB&Yd4#SM?{|i@*GYtM5lIy1)Fid)7?<`la9d;N5o~zkC1jJOBFo z@7|w`{jF!!)9!6%{`PQEBXCKFk9Za|65GhfBf!$czpl; zKjim^-~WF2{QAk$?!|TW^!feY=imMKd+)t_|M%bhgLgjs{^R?<_u&U0@bx~DH~;AI zJMaBq`-Ok|O)&q~z5gP}fA`-1yY~C{oTF)b?<-P`ycoI`@R2m@4s#y z|I&Xr@qZ8zhzLXkA_5VCFA@STf9=kj7k_sr{p$yR@Z{OUSEJS!8OWJRfxE~)c2X-%G1w&Lsj#pR3Z_c#B?%|GGo2Y3qC@-Mjh^x3l~ z&pu{Q^Ze6~K7CPr%)2_3kCl6MFWK7j#etouoF;Y?{CQ z;8}P5(bZ=U9wmiUo4;N2H~!dsdUe%3yZ$H~;J@1sWWbLvuYUC2WxEfhkJ@fsefr7u zgP*+}ASvI|!?t^RIY3vpWvO+}ziZvCZIQQ4VN_8XV+vIzd8JL68mm*CY29RNTk5LH z>aH!bI!|nK3Uqvz9#VNx7Rq5FZytcEG&)br2v%BVS(@lw$cBMa(6$p!hi*X4q?M`F zx-cc!G|gI9>&kX%vhIr77HL}NT~joLDowjKow514P@UGT>aYrZ2u#*&ppc3ui4y^ErFd$5-8p0Z)@=wkbi-wrlpWgoS9rT&*pC zUst)#N^9F%*S4)1z2<}vLo!umlqzl7WtGkRhK~X);XI$4Ly*8|^<5d-P__#_;`*pFI2Q2UnM${p7vs8K>aYV32Hm zdTr{q)lI=cQdAwMMbTAVm84DCX#T~WDcYt^N|mp5p;VP-MNyTV1^%u+0Z)SslxD@T z&WTzSHVxp+IKC{Fp71Vy(r*gA{u|%q<)40om+yR?m$$yg%Rl-mFTZ-1mwR8~(^1Bj$wq2=Oos1U?c}vOFP9DIpm@7Uklm*=D5wemG+}iqG5i!|Th3 zPul@X@GEPb*V^hjF+@olUFNygRgvgU8QUtNWm(&{SzYs!%L!U_c~Tcemn*{3rw|=! zLR8NrtCDD#5I;kFZ%ZL1rbx5oc+@Y+Qspddz!EkF%=Se5wj-xlZf7iqQjAu3qw4vS z0nDUJvj+aGtM~>|RkdYZx7v36O)HGw@SPVBOF(N&!udL>G~80%RE6zyBh0|k2((qI z;7hKO(t;`xzaZeJb;8@3Iv&!8a!_TNLCp|>-QsDrDismMoXE5fotS4Z!++@FsQ%1f z2-|>`Ge#2=Z>%G0< zEZ(Ntt{~&IGT$FJW3ph67fYjNCaVa1Z1W#z^FGeZ6lvtq_RYGSe8X#bJhQ^#2$o(H&-HQdu* zT)o)RCu_KotSuYc!s!($_F1gkHJfWwRcTT+knBaK*6gqFq_%F?Ra>m#Pgwt%POIXC z)vuvDRlWjC4S&jZ8d@i`R017514paCk*#>NO`}se@%ErNj-8^oofBd>=Xn44vSN*n zV3t|6&NzM`kMS9lE-K9t4s(~ZkWKK$?4QsnwXGrGAag6twqT*DOlCFUo&aV6jhCz( z%b&@ym0xvd-mda*R|IuIEMwj zhC?!0it#lp*|J?1X##WHvd*)F72a}6>bj^C5r9BEr4~XN3q&olPU|c^0XwZUJK0KJ zHot+u+a{AVV;>zLJ%FAq;7q+85(Bq6t0vvEoQJVfApJSZ4Y4=Y=fdY9s(CibX^=GJ zNmnLmm)g2%@A3->&mDTbNqoYNWbl^G~6EyEa?5sj^9nvIega*N$awIgylIr|YHzmOMGd1?_jl z{=7-$Cxo4CO!kmtZf7h< z@;vVUIoEToa#H?#t0tCW3XyYv$rb3!g=&psMVn8Hh_ zXFG8@x@U0zM-_?txiV~qID9IMT4gp*3zN*b|Kt87>_Df^3wm;2Tl9sFA*IUfA4J^aBLH+%G~lrJRI@H5NGZ|aU8pY;xH$~ z@Rr>F)3ne?qeUM{Xd4-2a4fMiDnen$BYjqBkzEe=$Qmgtox zoTY7sr-UVW+a%E`Te8%T9_d^koY4*F`FD9AFL%A_$NKNPGu?|BEC=+g?*9b_rwhH3 z7x^zd-7QSioa6{h#}anG?k4LJw$M zU)W{`3ni^UVyxnZ`cU=v|Lt)9+2;PgowgjrGrj-gfTCf~XApM^I!r=(1Bird2}ZGZ z?1D(_cQba1pf_hZf@gOBm*DciEFCzebL62LK$x6 z%*8FZ|LgKmY93|E>HqIG7yODJ5rK$6L?9v%5r_zUeh9?;e@Alcxk7IW1WY}flWx#M z95)Cw#{B;|L7M@(fk}(`|6@Upy&BfbpT_+EZ5E`q2fX;K2k(*J&?b9FiVD!Q`ojMmfy+FFd{%@KP1mbJ>G-_r_7@yUsVxsGinOi zt*riFTwhxIv!G(Y!;)NQ4$Vm{AhUO;uy`>-{mIZbho3YNRG3XTi+Q} z1Z16gx|fU;CZrd~t&gl?{?ndr$WV_Jnd}~Q#yED0-8d)2?tqBx<_iF_U6D1uLP<$} z3-uTS01;bYl*dI^097DJ4aC3&3Kwz#v1lJB2>^Jb{YP?S=yBF;49}?mpr(*6P-Nt* zf-2d8{n{NAfgHmmt4OWgBS}NPjvpjkOl7Oz$1{i<*2IfHH$^AvHl!BV_EGwT1B6x_zOlYEI4 z+AFkaSb+{}V8+fi>dy=}(j~+awG{$d z?gsMzts-Mi6)QzzDya~1!g$8YspV=WAH`SX{1j54WP#_Ji;TaZHp$+S75;=Ny#B=H z=-#~kUk;&CuPmiSQZgbY89Z?ykN zP9|?wi01*<=?wD!bHkEfxoj$zOckoGqx^r#PRdrN(iIj-Mioc=p!Dx|(kTBw-1mVb zvm{-S9M;E*a(jr=|Z>CP~uIR^*rSiP!LQWO1aae0B8f5jj8kR*CU0=OW`L zTikHT+}$Ys2|GKbKQY`Z=bY?ks{hY&^39F{+3Wuk>-k7d|BoAh#rsgndE*cFpUHva zObmL5j;Ene>TVdMAJ`xnzxKa%THMhd`jQI#Q4Us{u431an4-acJ%-D zk;*pw|A!ZMKiuId;u{fxh(JUjA`lUX2rNP1<^3j)C#w zW0By0b+1GUv!;Z>RuoHee+aJ3!uMNxG9JB?Bqg1~q}(#joC|;vuq`u*sQG@M3x5qG z{RiR@ZfDH(2S*L|S@QkRpfhlLP5FMB^p%wPSy70%Fs$9c>ZBVUeJ{C$MWF&htX5xgYWNWEw{ci;QOiIo0itG*L2AfDmws_cPQ{XBPCu)ePjH> zf-3>9Or~!9*AP!v;aV=VV8-eusREpvV|cTxPbqs6erZ`xDN`GkJSmg z=))vCA^iU^+J7W_p*L$bglEP7Q+gZdd(Vf^dF#Ue4-@={g0DYuIlMQ||EF9O>HM+kyHHd0IoJaz0EmW- z8|86Wja0vcUmQUdDnCbTzQ=LG#)r{UM7>!dpa)#1GY|l9(q?8w6>swOr2J<9W90vr zY^8MOFhGg0ugGa2G^w>7`TryQKPP%Y6o}^J)JfeIQyUJVSpV;&`v2(XQ$AMmA|IR` zo~tBM71sUHO;YImlJeBz9Sp9#@c%gJ3jNuhv)!Apvr+mJ!>w}8(f-Bc|8qT+JIfJ% zl>gsMTMlC6{~fwtB>sPP{Xg!YxJAP<7A~K9wVeC@I(7cJXa(F@|A{z{f5rK$6L?9v%5r_yZMd0O~FH->EquV48AmNbkB($LW!?r_ie=&^u zfLH4S!c^iVf&fz9y{n$k`-QZ~^c_JLVl258PCE~ahq#?F*FlVefZJOD01AON0}!y5 z3_ur2^+F5?0(fsu2T>?~X_DqEQH0tj8~GK2fMK+6m12D3tmW2s27-Wa)e8Tg!K5CI z(Kt?@zT4x~&T;u!vL`$qU*C58VL;yf?rLeMm z>yE7!D&Js!yyfF0JD&&u`qBO)F+XqCYz)sz0ATo@O;N0b83cilrWXDm+I7Z)7XE+W z@dre={5^il*Tr|)MV4urSO$%*!e?$}3;i$sTyH1+FVPeT0^%+1S74^G{oexm9}}c) zA3<#zXi^CvT=0?qUqV`G7*teaaDeh6{{?1=rmBP`n9)hi2>{qIf*#;5|DU$+q^O6h z*##Iy05Hth*+rcIpr0A;qjOk-GZFwm2pUV))d~uWPuqS={RIX~<|i;CchCs{@TRCI zFdk8k$7jB*0Utu=or?fqnBYH@t<;~m9NwE308nQ_s#cJcigCiNhQyXM0HllGpk_JH z5yvt5CF4g-g~@l!%N|ID|KE(B0_x2Q@jT!G(1VDPZP#?@<7-WG^iVMkrcR zLYJaQMSUA!83h0b1pwrL*nBnQfZ$^W;NdaF{(mR*SJyN_HF>V+?rT%INd~A&O#l-6 zH}4(TBpv^cF2fHBIsQMhCGzXtebS$>vr!8FKTHg_$~h-{n?&GB_ zMjRgzhzLXkA_5VCh=7m4%YXSM>HpuoMf^SqFp5krYKZ65;CH==y#Pd{_g_u#C)S|f zCIps~b8RTS8s6;|%v(TZG3 z_zRO~yvVCC_oN5!;amOD)9c7MYwmSqRGXXu-%s-2fe5Ut2mnq;V>FJ_w?Q788Wyc#cvnKc{2bE5yrP=$-8o&xJL(Z!@nz0m){Vja&J@_BrK*rxw)_?Yt1~IkCj{s`~NJdbaJT18uviE8D6~8&ku2W)(RQ zwQg(EQEl2ZN)#rlwx&`|1E;yp%ew7MmDfg}5S&e{mMAJgZ;o$34}-I@Aixqu0E(5Q zT%|=?P|Q-!6wir_ihw077A1XzrKX;We8NOSELjzb79l;the!kHeF|ZtBJxgn1OZl& zl&Fe5RaHbnu$?ho>A}nsmilG$BjKOUpdvu%Fv0(`&(7(PB%~r$FA~v14*MoFKINE@ z!%X(Zp8p>wOyTq=E=Tj``G4p_Tt_ZHxaUY`r}TdcT}VBOqh6CJPzvO1p4u5ApZypG zT6o$xM90xnJiS>VoCiFoGob$yWhu|GLN!To3i7T89m*h1K~D)uczm%Ze3_i4J)UVY zm5abWUw$0BgW@nJ#Bk18{_$niG~J6A!+Bn2)tYKrZJMm9XGBhZr8%#yX_Gdq^tw>x zT6di8wuTy2wyd;j=v2mK0`Z5f+Y`=nfI^09GdfPCVwIs$M*jaG|IZyinFh(D6e3yjIM?ApF@J@2^T*cKY_uY^D2|H`t zpSXOTpDF*Jawj?tWS`KVmmUKPk&lkG*jI4VA3lZAo3Tv4I`;ML44xm#2Zhc#IVbHcCi%4}g`f$wt>C38 z;Vxm@!4io+(qqu-3x9Or~s9*Aq-xFWz3PNC2`6akcp%Ov;MKnen|Gxl5M0r)b= zL_K=@}4-hbWHKMR3Yur z$xzK>&zTd3Nz)IEdmvjvoRbuTagYz zV+EP5{5&kWf_+ZsIC=`G8E1uf9&nV-KmY(!jf_I->fpF@EOF?1I7Bl2@ew+-0Ln=JAEf_j1gA2}|H~(N4eck&EG0pe6iyJ$-8|o`M~~krdqN*iYYcHXB0nvX zJ>fQg_!LHO+Hw#h|1THK*OLDi)eeS=%?$bf((D!8o^N11|KE@GpA7u}X3kv8+u{Gs zBds?1|9^FH_pg?-9C1cOAR-VEhzLXkA_7Yg`1#-5r31j9+#-EI0ck~pY-#pN=V7{( z2KVYH5O}pfpbskc$sDj=ni}oLY3!7W+NocM9o9YDD^T8!oVtkH8FM|vv(f{ExC#AF za^`Wu^);pcbAtR7P@ss1BND>!w$uYaT5;UyNYSB>tx7EOhs#MxW)9_@_XT3T(No(S zXDv6rGhqKG#z$U^ayK>!%O0SsVeknTNbQ6HJwwhktrzyh6~?hs?8Z4Eb_askH_i5w zZH0aZx}R8x99G4e<|$ zW=RU$*!S%s{vnuPS@de#8JKUvgETV#^O=7N9!Xqzg&FaQ*HAcMQJOl+55wf0uN@MNtb3GyI3L*n^qjCOU^9 zI3w;~D;PnNpjZ6f{0a~zdD2p0rQcx>?4^$TC)b@s{}EtE;UlgzoLN4ETke0D;6L1Q z|NX?}@ZLQ4pHS)}D^pl&k%s1Kp!xfIj#Q`6d0HH&V}$(YF!Mnb8o#u~UOmM)dWxtw zD+Kg_-*g7#zouW5rD}@%B(J@j9<%>X5Rn&X7S)PoXqy@x6AgK!{}0mt2HM)0Tr6`Ty6K z|4&^sZm>Sd|3|XBPoB%#-i)1^-Z*EjIrUV%yk4K&3|jN{HZWO+dkS?7YTs?($a(V3C~XmXHLacn0vxI)EykJz{1#5xp~hG zSX2SS=&A9IvzA-m8SwmMQ;?hv%_^VdHQeyX5T2dpk31z0()=l+srI!5cErCMI|WmS z|IM5b%mV@J8z=s`$WmvufCrFIyapBt^G{WZrUYCrNzBuY4|wFR_KlP zA354;oHZN6b7KB8v=cOvp@J=nozzOot`{q$)C{RYL=iXd9w4iP_#b$Dfaoj#kNmyh zhTrmQ;=Az29SHzkN+ox03NOWl`H9l;auYkdWfA}iM$^Iw5Zx%x!e147&EB6TM4UbP@ z@qH31rx_S#OxJoaGu%w)a0F+h0H7WMUQt<)+Dc7%{uH`R((gd}G9TSkodST|*q~pM zQvg_ecD|?&;Z^}KOzeQiB3$Tm=fP(@6N}f^2Fp*dH5;~ue|38)g7fZ}&Ziqr3CECzOZpa8R zIrYu(r^U}d;L*eXPs1bo6!{mrsV>uJKWYkE*aTcDjPcIdd&<5C5O_{C{Jt45#p6ZsgP%6bXQvG1nK25`bGP0Z0?#`@{=1Q3L)=b@U*_B^rP{o0HB4 zt51t`stu2p2LlV;Al24=3v*FdZ?ykRf(hfS<>q$=8URv;=)Ja*25g&;J{B`%vMtg8 zP>gYe)A9LqXWWI;X#m{VDVXD&5X=K1>>JkrsFL3B%92}c-LEj{x+MF*MCF9_4LhOJ z0F=c(YDF{v{pcx#-mKXeo|6WEqRGYC!vmxEba$$%Q%0^B%~?DS5ehbMS>Q~0h*5Ur&#cD z?hn}0bfbE3hcx*b#B(KX>zki!mBUE?eZg^@5nAP(fa~*i{qXwo;Zx~|{G_E8a@D3~ zt@B!2UBjQ{X`{=WeXJ@H-BCKCg;29u+qPLDUAPi#?(TCnahpzj2M3i)l8cNTXnTeq>YI$qv94rPp;!CDk1#&3UsIUPIV#t z@w>{swLIl4Db_$N@&v_e81F1RPOFl`d&X8Oe7|(7@h}y>pV^`m z^ido5{(iLo$gx=CtPsxw&eA#X{Y8=G;=l9By@tja%l%jLg=nmDGOHyun)8@@Pq>pt zzW*TKPt%?B^#|7RW;{)A~EJ~Rk=|e|5bU&&6WaEt2 zA))&v0>pu|yH5(;KTO!!DE*1yRyoJ}JX5;gs5w5;hQF60KqHVYZ{N5wbv{&b-1x)& zXL83)Z$aqHPP(qEy@JHB|zo7zRJpPBomZ zT+c|WZ_0*%7HJh(t(v5xuw7QIb$ZJEhroEkwVdi_T)330v*r0e+{Cy?$`$dbkcWC5 zl;a%DQMi1*;N~rnUWvYn9jw-8Z=4 z!)MtE^CIztV#pdi2pH&9;Ny}ICEzHGTVf=^D{xe4jZf7kwzcXO`iQbd* zw<5R#F$QUthWUVtvi^xWdeR-o-k0+Ae8>b&Vf;5^{U=C?PJ$qe|1jErBoKPDW@C6xj6YF5qJmVe zMMfb@T{JmfXhG8c4ZW6+pDaF)4-(t7|AD{vfs_3Ie=M$>u>T~+nqpdk&@FXZW{V22 zmkHf2_Ma4gI(M+rQB0BHD~%;QSYyKfe4d4XHeMsDGel$PB}CKYAsNa4CFDOy+9EGj z2mmB)deG{P`Tq3ZfnMDpeBeBX%cqh>ULyDKHc;6A6lEEW?svD)V1~aJxJ}fb8E&I< zNP;tB|6!>O$>Ry7tWm|H{|V1`D*jVwVMg~9q5nlKVTD#4wo{=}s7+6i>|K0=2~&Li ziOb==dHSC&^wK_IwYQ4`{lf8mNM1i`wXpxjk`L=$e_;Q!0?q0ki}glN@$_bea2{}+ z&Vc>rd&U2x7`RxdGtgiwjp_bNwo=f_6y?x3-|2p4Cl~Ta{~xCRxlK|Oh5JvUtf6yR zxJ^d>|1|y|E2NQ6x)Q39ItrRy&2yEcTgsemQt13V7p3KR^a%fNp-*XqxO?{a6Lz*r ze`2^<&N%8+`6RF5C4CY9Um&L+*EL=|#lOVk_bqPV|2Jd(C*rM-bLN`jl>ayCQQGtW z?Zw^pbtZA*_Z|_52t))T0uh1FAAy&@`i(a){`PNfkwgF?eQ8%P()h^yX_D#{6#}nT z2=wu0f;r|KD9@nD?rw@7Wu`j~^(>4XVz(;7{|2Jd(CvuY;=Y-fD2xH$g|Buw3q|<_u zmV~is858oV3+R8cE3I$_!?p*JPxX6pn0En${vSq95%gxwhVY!|f0${K3RXrX8Qqjg zWD6pOgt!SQx?UZ}LR}t@KOjoF!j`}Hal#Au; zn_lN--FBwRYokv}=O~HwLBtGMn4yLxRt8ug4uB=4DgbR%(5OiGJFwp<4p_oyNf`%d zeY>I;PbQVYCt^R!^N1MdxD-gZeyaIUO2pq^;rN+c;@Iyl^( zo;6L^C7n58;fII4DX7I@XBPjbFq%S6FrHWNDk3#-EpB|o4I?UYn-RRW;gN+~grKdQgDOQZ;Qpn;&v0U45vXIFiTkKINXOCsBj-hpvqC%% zxKU@I20(bH@)ZaFM)xOmCOon*iUF2votfezz%L2_4hsM@0)T8KTQD`I>DDnhxTPmo zHX~P^BOm8+T)~={5tXZ4DKCGW$`T&N`tQ5*#hVi@;90&T$4hth@cGr{XFqwke)8<0 zc!aJ{lGRO~baZ#?Qd>7o-q>|XZ>*Z?8znnblc`n}Rne7=PL-}v-x7FOw^^1Exj9V$ zKo1XYB*}_8037CSZ}Zl)2=FQJv$4|yp$*t+cDvMQk0?X{kfo?y`1*T}Z*RgB-C*K! zc+XV;P|T^^)ie71Bt{DrCZF4cNcVlHoPi?ClC%IB?mrVF^`mxL87*vouo~Q^m zW2az>GGH?&1oJ>h`=*rvWVdN*15j|CtFqjYL;4D70KG+z2fw5~Fm=kMG!5UnOJgUb zR=_i4dm>-n=qZHWtl1czlQe)56?6eeSCloBf`+cpw--!gfm0ul!pTR&i9V0tLyAZ8 z&%`V7F>?{HlAK~vk0hAxKa&WU#6ZZpAui#5GPkQ7Ktf6UA|YQ8ElH6TgTAbnO-06k z0pm}uFWkY3EpEddOk!cun#QmN95qW0FvK0+K1s=2dgbn6r?(@gCb^vvF7UauLC;DL zd;ujwvnFqou7co9Rn?Yt-D=zMH@fVy1X0sK8582~Y`NBzPAUTOZQWFb?R3+fKu9NS zYPfhSWHuW;Rd|RiqTH~oEVv``AK~vwOk(N!cnaeW!A;I_xSCyf(Vww{t3UG>!uF;a ze;Oi@hG|!>a)p2#p6aA)(rtbYSuKoyA(dajvfo418rud2kV3@cZ z-ZPK@kg`^0MY^K;6#2sd9M~-y0D{*PvN&qDZ3Q+^FL<4|hVz>_3962Kj>52Op^#Dio0F?Mp zG);lQCJccNH(1fJAi!yQ0Q6yDmx=&YOEnZTBQhj<0EFq8I90>hBeXs{Ft_wQ_ey`l z&Q9r14EM@8e$$!i0T7+cD1+GR0jT{q0_li-(A}du_lHkm^rkHbG3o(0&2O_FAk>;I z;{P*b{-gB?m(Rl9<3I}kzZp9ZT5%iH1qssFDx_5c6;;_jbIBp`l71R??vfrvmv zAR_R2An@|7Z_ys$?k(c^MO|#j{U<_>>r0)>;IN3@u2+FepxY z0BS}J843s3SlU%V`FPhn7=5}lOxPHSg>6Spy~FK{xz1tK0Nh#)KxWCkq&zbs0Qx!7 zAfRUoylMal+Km;1Wck7K9UjdMcm4ur68p7Tf9YKUARfq8Q9i*n6qr zjz#)7$<`;Fe?QuPkzvRm9E! z3#0%k9RVmKEHx}po2Al3!hK*atQcrnz8A`dCt@!$52Gw9Ek)68DjO zPDy3NeIM37TPu<(+N(&A+}|{NDZn_xf9Q5pf945m{d|)DDHpyYV}}1vHbsz*2rMve z>C-}Ht9QslDmzF0|rTI4H}<|@$t2vgY* zHWqxqRw4Asx^=|@21`XuKua9H_#Q~d(NjRZSs|VWe5W&@|0(aoDZ5H@l@>^eChb6= z|Iq}@Sn?DN;T%VYGA?-PfFW4HR(+(LU5;b@M`D%RIU$5|j`WW&E2?t5cri$+N;)mW zskLdcruCK5MWrd)V@;d1S*6#gL)W^a#E-4nmy|6lO(75pYHe!K*X;>XDrr(fqcls+ zWD0T74LaCABmIAv{^tUipprtk*Y`IJ(2Ml{Y4ktLo^%XfvAok9S@Wb>8vH+9YWh3p zfo&2xKgYw$MQ66%C*|Pb#%^Cx4`4fCXO;UCm#_3Q<^PfSsos%40OW?ym&%bNkdDZg zN@u%!r9XTMqc?3ih>`#2xOqMKe_D7*ICogaT(I$X_Emd@|L@06O>dkt*YX1XKT(gA zCWx}(|9^II_h+96+Y!G;L?9v%5r_yx1R?@Q5%~FM-=+w_d$&j&fY(e{0Aju5d{DSK z?Y-@uYK-E5SBnEeQV}hHA-($eWrYx~rXbnqD{_a-$-mycr))<~J;m*exsKwQYXOi} z9LMc7Z47{J#1vV%Vto=$B$yUPaEK*J9`NyyEc63a@Q6K*hDUHws#A1;G^ke-eE)=_+6JmEDoPE<$018G(IDche zNhs_);R5G31|SA(Fssq}Py&>YUlP#uVG;mQ0t}=5M~=4|XU&H2oRk1kl9cb9(4ka& hp_yoS-je-KGBRaCk1l9o7zQ36BsSU}{F8t2{{e@KP}=|i delta 54 zcmZp8z~0cnI6<0~iGhKEWuk&TBh$u&1^k-=SOr-5zccWE-z=!`lz(CX2PZotvnXdu KVoBm60|5Y$9u8>$ diff --git a/misc/dependencyGraphs/mermaid-all.txt b/misc/dependencyGraphs/mermaid-all.txt index 87ba7da..0fc2d66 100644 --- a/misc/dependencyGraphs/mermaid-all.txt +++ b/misc/dependencyGraphs/mermaid-all.txt @@ -14,33 +14,33 @@ end subgraph 5["utils"] 6["dockerClient.js"] 8["containerService.js"] -N["extractHostData.js"] -O["writeOfflineLog.js"] -U["rateLimiter.js"] +O["extractHostData.js"] +P["writeOfflineLog.js"] end subgraph C["middleware"] D["authMiddleware.js"] +E["rateLimiter.js"] end -subgraph E["routes"] -subgraph F["auth"] -G["routes.js"] +subgraph F["routes"] +subgraph G["auth"] +H["routes.js"] end -subgraph H["data"] -I["routes.js"] +subgraph I["data"] +J["routes.js"] end -subgraph J["frontendController"] -K["routes.js"] +subgraph K["frontendController"] +L["routes.js"] end -subgraph L["getter"] -M["routes.js"] +subgraph M["getter"] +N["routes.js"] end -subgraph P["setter"] -Q["routes.js"] +subgraph Q["setter"] +R["routes.js"] end end -R["server.js"] -subgraph S["swagger"] -T["swaggerDocs.js"] +S["server.js"] +subgraph T["swagger"] +U["swaggerDocs.js"] end 4-->6 7-->1 @@ -49,22 +49,22 @@ end 8-->6 B-->1 B-->7 -I-->1 -K-->A -M-->9 -M-->B -M-->8 -M-->6 -M-->N -M-->O -Q-->B +J-->1 +L-->A +N-->9 +N-->B +N-->8 +N-->6 +N-->O +N-->P R-->B -R-->D -R-->G -R-->I -R-->K -R-->M -R-->Q -R-->T -R-->U -T-->2 +S-->B +S-->D +S-->E +S-->H +S-->J +S-->L +S-->N +S-->R +S-->U +U-->2 diff --git a/package.json b/package.json index bd38ba8..1107785 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dockstatapi", - "version": "1.0.0", + "version": "2", "description": "API for docker hosts using dockerode", "main": "server.js", "scripts": { @@ -11,7 +11,7 @@ }, "keywords": [], "author": "Its4Nik", - "license": "ISC", + "license": "BSD 3-Clause License", "dependencies": { "bcrypt": "^5.1.1", "child_process": "^1.0.2", From aa4a20fd5f4af1f24759754b09e52bd3053af205 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Sat, 2 Nov 2024 19:07:23 +0100 Subject: [PATCH 008/324] Added icon and url routes + adjustment of methods used when removinf information from /frontend/... --- controllers/frontendConfiguration.js | 118 ++++++++- data/database.db | Bin 126976 -> 577536 bytes routes/frontendController/routes.js | 347 +++++++++++++++++++++++---- server.js | 14 +- 4 files changed, 426 insertions(+), 53 deletions(-) diff --git a/controllers/frontendConfiguration.js b/controllers/frontendConfiguration.js index ff1ce3e..2ba90e8 100644 --- a/controllers/frontendConfiguration.js +++ b/controllers/frontendConfiguration.js @@ -2,7 +2,13 @@ const fs = require("fs"); const path = require("path"); const dataPath = path.join(__dirname, "../data/frontendConfiguration.json"); const logger = require("../utils/logger"); +const { PythonShellErrorWithLogs } = require("python-shell"); +const expression = + "https?://(www.)?[-a-zA-Z0-9@:%._+~#=]{1,256}.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)"; +const regex = new RegExp(expression); +/////////////////////////////////////////////////////////////// +// Hide Containers: async function hideContainer(containerName) { try { let data = await readData(); @@ -19,6 +25,7 @@ async function hideContainer(containerName) { } } catch (error) { logger.error(error); + throw new Error(error); } } @@ -36,9 +43,12 @@ async function unhideContainer(containerName) { } } catch (error) { logger.error(error); + throw new Error(error); } } +/////////////////////////////////////////////////////////////// +// Tag containers async function addTagToContainer(containerName, tag) { try { let data = await readData(); @@ -58,6 +68,7 @@ async function addTagToContainer(containerName, tag) { } } catch (error) { logger.error(error); + throw new Error(error); } } @@ -77,9 +88,12 @@ async function removeTagFromContainer(containerName, tag) { } } catch (error) { logger.error(error); + throw new Error(error); } } +/////////////////////////////////////////////////////////////// +// Pin containers async function pinContainer(containerName) { try { let data = await readData(); @@ -96,6 +110,7 @@ async function pinContainer(containerName) { } } catch (error) { logger.error(error); + throw new Error(error); } } @@ -113,9 +128,107 @@ async function unpinContainer(containerName) { } } catch (error) { logger.error(error); + throw new Error(error); } } +/////////////////////////////////////////////////////////////// +// Add/remove link from containers +async function setLink(containerName, link) { + if (link.match(regex)) { + try { + let data = await readData(); + const containerIndex = data.findIndex( + (container) => container.name === containerName, + ); + + if (containerIndex !== -1) { + data[containerIndex].link = `${link}`; + await saveData(data); + } else { + data.push({ name: containerName, link: `${link}` }); + await saveData(data); + } + } catch (error) { + logger.error(error); + throw new Error(error); + } + } else { + logger.error(`Provided link is not valid: ${link}`); + throw new Error(`Provided link is not valid: ${link}`); + } +} + +async function removeLink(containerName) { + try { + let data = await readData(); + const containerIndex = data.findIndex( + (container) => container.name === containerName, + ); + + if (containerIndex !== -1) { + delete data[containerIndex].link; + await saveData(data); + cleanupData(); + } + } catch (error) { + logger.error(error); + throw new Error(error); + } +} + +/////////////////////////////////////////////////////////////// +// Add/remove icon from containers +async function setIcon(containerName, icon, custom) { + try { + let data = await readData(); + const containerIndex = data.findIndex( + (container) => container.name === containerName, + ); + + if (custom === true) { + if (containerIndex !== -1) { + data[containerIndex].icon = `custom/${icon}`; + await saveData(data); + } else { + data.push({ name: containerName, icon: `custom/${icon}` }); + await saveData(data); + } + } else { + if (containerIndex !== -1) { + data[containerIndex].icon = `${icon}`; + await saveData(data); + } else { + data.push({ name: containerName, icon: `${icon}` }); + await saveData(data); + } + } + } catch (error) { + logger.error(error); + throw new Error(error); + } +} + +async function removeIcon(containerName) { + try { + let data = await readData(); + const containerIndex = data.findIndex( + (container) => container.name === containerName, + ); + + if (containerIndex !== -1) { + delete data[containerIndex].icon; + await saveData(data); + cleanupData(); + } + } catch (error) { + logger.error(error); + throw new Error(error); + } +} + +/////////////////////////////////////////////////////////////// +// Data specific functionss async function readData() { try { const data = await fs.promises.readFile(dataPath, "utf-8"); @@ -176,5 +289,8 @@ module.exports = { removeTagFromContainer, pinContainer, unpinContainer, - cleanupData, + setLink, + removeLink, + setIcon, + removeIcon, }; diff --git a/data/database.db b/data/database.db index 619beed4fb9bfcf112978dac7d78da14ad6617c9..6535b160318c34abbe5c6debfa6291df728eb2f9 100644 GIT binary patch literal 577536 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

?G{#>66I&Ykj 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 diff --git a/routes/frontendController/routes.js b/routes/frontendController/routes.js index 986276f..ed4f127 100644 --- a/routes/frontendController/routes.js +++ b/routes/frontendController/routes.js @@ -8,13 +8,25 @@ const { removeTagFromContainer, pinContainer, unpinContainer, + setLink, + removeLink, + setIcon, + removeIcon, } = require("../../controllers/frontendConfiguration"); +/* +____ ___ ____ _____ +| _ \ / _ \/ ___|_ _| +| |_) | | | \___ \ | | +| __/| |_| |___) || | +|_| \___/|____/ |_| +*/ + /** * @swagger - * /frontend/hide/{containerName}: + * /frontend/show/{containerName}: * post: - * summary: Hide a container + * summary: Unhide a container * tags: [Frontend Configuration] * parameters: * - in: path @@ -22,10 +34,10 @@ const { * schema: * type: string * required: true - * description: The name of the container to hide + * description: The name of the container to unhide * responses: * 200: - * description: Container hidden successfully. + * description: Container unhidden successfully. * content: * application/json: * schema: @@ -51,15 +63,70 @@ const { * type: string * description: Error message */ -// Hide a container -router.post("/hide/:containerName", async (req, res) => { +// Unhide a container +router.post("/show/:containerName", async (req, res) => { const { containerName } = req.params; - const target = containerName; - //console.log(target); + try { + await unhideContainer(containerName); + res.json({ success: true, message: "Container unhidden successfully." }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); +/** + * @swagger + * /frontend/tag/{containerName}/{tag}: + * post: + * summary: Add a tag to a container + * tags: [Frontend Configuration] + * parameters: + * - in: path + * name: containerName + * schema: + * type: string + * required: true + * description: The name of the container to add tag to + * - in: path + * name: tag + * schema: + * type: string + * required: true + * description: The tag to add + * responses: + * 200: + * description: Tag added successfully. + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates if the operation was successful + * message: + * type: string + * description: Success message + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates if the operation was successful + * error: + * type: string + * description: Error message + */ +// Add a tag to a container +router.post("/tag/:containerName/:tag", async (req, res) => { + const { containerName, tag } = req.params; try { - await hideContainer(target); - res.json({ success: true, message: `Container, ${target}, hidden.` }); + await addTagToContainer(containerName, tag); + res.json({ success: true, message: "Tag added successfully." }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } @@ -67,9 +134,9 @@ router.post("/hide/:containerName", async (req, res) => { /** * @swagger - * /frontend/unhide/{containerName}: + * /frontend/pin/{containerName}: * post: - * summary: Unhide a container + * summary: Pin a container * tags: [Frontend Configuration] * parameters: * - in: path @@ -77,10 +144,10 @@ router.post("/hide/:containerName", async (req, res) => { * schema: * type: string * required: true - * description: The name of the container to unhide + * description: The name of the container to pin * responses: * 200: - * description: Container unhidden successfully. + * description: Container pinned successfully. * content: * application/json: * schema: @@ -106,12 +173,12 @@ router.post("/hide/:containerName", async (req, res) => { * type: string * description: Error message */ -// Unhide a container -router.post("/unhide/:containerName", async (req, res) => { +// Pin a container +router.post("/pin/:containerName", async (req, res) => { const { containerName } = req.params; try { - await unhideContainer(containerName); - res.json({ success: true, message: "Container unhidden successfully." }); + await pinContainer(containerName); + res.json({ success: true, message: "Container pinned successfully." }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } @@ -119,9 +186,9 @@ router.post("/unhide/:containerName", async (req, res) => { /** * @swagger - * /frontend/tag/{containerName}/{tag}: + * /frontend/add-link/{containerName}/{link}: * post: - * summary: Add a tag to a container + * summary: Add a link to a container * tags: [Frontend Configuration] * parameters: * - in: path @@ -129,16 +196,16 @@ router.post("/unhide/:containerName", async (req, res) => { * schema: * type: string * required: true - * description: The name of the container to add tag to + * description: The name of the container to add link to * - in: path - * name: tag + * name: link * schema: * type: string * required: true - * description: The tag to add + * description: The link to add * responses: * 200: - * description: Tag added successfully. + * description: Link added successfully. * content: * application/json: * schema: @@ -164,12 +231,12 @@ router.post("/unhide/:containerName", async (req, res) => { * type: string * description: Error message */ -// Add a tag to a container -router.post("/tag/:containerName/:tag", async (req, res) => { - const { containerName, tag } = req.params; +// Add link to container +router.post("/add-link/:containerName/:link", async (req, res) => { + const { containerName, link } = req.params; try { - await addTagToContainer(containerName, tag); - res.json({ success: true, message: "Tag added successfully." }); + await setLink(containerName, link); + res.json({ success: true, message: "Link added successfully." }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } @@ -177,8 +244,138 @@ router.post("/tag/:containerName/:tag", async (req, res) => { /** * @swagger - * /frontend/remove-tag/{containerName}/{tag}: + * /frontend/add-icon/{containerName}/{icon}/{useCustomIcon}: * post: + * summary: Add an Icon to a container + * tags: [Frontend Configuration] + * parameters: + * - in: path + * name: containerName + * schema: + * type: string + * required: true + * description: The name of the container to add link to + * - in: path + * name: icon + * schema: + * type: string + * required: true + * description: The Icon to add + * - in: path + * name: useCustomIcon + * shema: + * type: boolean + * required: false + * description: If this icon is a custom icon or nor + * responses: + * 200: + * description: Icon added successfully. + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates if the operation was successful + * message: + * type: string + * description: Success message + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates if the operation was successful + * error: + * type: string + * description: Error message + */ +// Add Icon to container +router.post( + "/add-icon/:containerName/:icon/:useCustomIcon", + async (req, res) => { + const { containerName, icon, useCustomIcon } = req.params; + try { + const custom = useCustomIcon === "true"; + + await setIcon(containerName, icon, custom); + res.json({ success: true, message: "Icon added successfully." }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } + }, +); + +/* + ____ _____ _ _____ _____ _____ +| _ \| ____| | | ____|_ _| ____| +| | | | _| | | | _| | | | _| +| |_| | |___| |___| |___ | | | |___ +|____/|_____|_____|_____| |_| |_____| +*/ + +/** + * @swagger + * /frontend/hide/{containerName}: + * delete: + * summary: Hide a container + * tags: [Frontend Configuration] + * parameters: + * - in: path + * name: containerName + * schema: + * type: string + * required: true + * description: The name of the container to hide + * responses: + * 200: + * description: Container hidden successfully. + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates if the operation was successful + * message: + * type: string + * description: Success message + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates if the operation was successful + * error: + * type: string + * description: Error message + */ +// Hide a container +router.delete("/hide/:containerName", async (req, res) => { + const { containerName } = req.params; + const target = containerName; + try { + await hideContainer(target); + res.json({ success: true, message: `Container, ${target}, hidden.` }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +/** + * @swagger + * /frontend/remove-tag/{containerName}/{tag}: + * delete: * summary: Remove a tag from a container * tags: [Frontend Configuration] * parameters: @@ -223,7 +420,7 @@ router.post("/tag/:containerName/:tag", async (req, res) => { * description: Error message */ // Remove a tag from a container -router.post("/remove-tag/:containerName/:tag", async (req, res) => { +router.delete("/remove-tag/:containerName/:tag", async (req, res) => { const { containerName, tag } = req.params; try { await removeTagFromContainer(containerName, tag); @@ -235,9 +432,9 @@ router.post("/remove-tag/:containerName/:tag", async (req, res) => { /** * @swagger - * /frontend/pin/{containerName}: - * post: - * summary: Pin a container + * /frontend/unpin/{containerName}: + * delete: + * summary: Unpin a container * tags: [Frontend Configuration] * parameters: * - in: path @@ -245,10 +442,10 @@ router.post("/remove-tag/:containerName/:tag", async (req, res) => { * schema: * type: string * required: true - * description: The name of the container to pin + * description: The name of the container to unpin * responses: * 200: - * description: Container pinned successfully. + * description: Container unpinned successfully. * content: * application/json: * schema: @@ -274,12 +471,12 @@ router.post("/remove-tag/:containerName/:tag", async (req, res) => { * type: string * description: Error message */ -// Pin a container -router.post("/pin/:containerName", async (req, res) => { +// Unpin a container +router.delete("/unpin/:containerName", async (req, res) => { const { containerName } = req.params; try { - await pinContainer(containerName); - res.json({ success: true, message: "Container pinned successfully." }); + await unpinContainer(containerName); + res.json({ success: true, message: "Container unpinned successfully." }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } @@ -287,9 +484,9 @@ router.post("/pin/:containerName", async (req, res) => { /** * @swagger - * /frontend/unpin/{containerName}: - * post: - * summary: Unpin a container + * /frontend/remove-link/{containerName}: + * delete: + * summary: Remove a link from a container * tags: [Frontend Configuration] * parameters: * - in: path @@ -297,10 +494,10 @@ router.post("/pin/:containerName", async (req, res) => { * schema: * type: string * required: true - * description: The name of the container to unpin + * description: The name of the container to remove link from * responses: * 200: - * description: Container unpinned successfully. + * description: Link removed successfully. * content: * application/json: * schema: @@ -326,12 +523,64 @@ router.post("/pin/:containerName", async (req, res) => { * type: string * description: Error message */ -// Unpin a container -router.post("/unpin/:containerName", async (req, res) => { +// Remove link from container +router.delete("/remove-link/:containerName", async (req, res) => { const { containerName } = req.params; try { - await unpinContainer(containerName); - res.json({ success: true, message: "Container unpinned successfully." }); + await removeLink(containerName); + res.json({ success: true, message: "Link removed successfully." }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +/** + * @swagger + * /frontend/remove-icon/{containerName}: + * delete: + * summary: Remove an icon from a container + * tags: [Frontend Configuration] + * parameters: + * - in: path + * name: containerName + * schema: + * type: string + * required: true + * description: The name of the container to remove the icon from + * responses: + * 200: + * description: Icon removed successfully. + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates if the operation was successful + * message: + * type: string + * description: Success message + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates if the operation was successful + * error: + * type: string + * description: Error message + */ +// Remove icon from container +router.delete("/remove-icon/:containerName", async (req, res) => { + const { containerName } = req.params; + try { + await removeIcon(containerName); + res.json({ success: true, message: "Icon removed successfully." }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } diff --git a/server.js b/server.js index f6a86f3..bf289f6 100644 --- a/server.js +++ b/server.js @@ -1,16 +1,24 @@ const express = require("express"); +const app = express(); + +// Utility: const swaggerDocs = require("./swagger/swaggerDocs"); +const logger = require("./utils/logger"); + +// Routes: const api = require("./routes/getter/routes"); const conf = require("./routes/setter/routes"); const auth = require("./routes/auth/routes"); const data = require("./routes/data/routes"); const frontend = require("./routes/frontendController/routes"); + +// Middleware: const authMiddleware = require("./middleware/authMiddleware"); -const app = express(); -const logger = require("./utils/logger"); -const { scheduleFetch } = require("./controllers/scheduler"); const { limiter } = require("./middleware/rateLimiter"); +// Controllers +const { scheduleFetch } = require("./controllers/scheduler"); + const PORT = "7070"; app.use(express.json()); From 0bbfc91e6d981665e2e28bbc2ee71b4d23ef4bcd Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Sat, 2 Nov 2024 19:16:58 +0100 Subject: [PATCH 009/324] Use commonjs for rate limit --- data/database.db | Bin 577536 -> 585728 bytes middleware/rateLimiter.js | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/data/database.db b/data/database.db index 6535b160318c34abbe5c6debfa6291df728eb2f9..caa9fbb0d7b4b30bfb4b2936cbb1e31f04e600b1 100644 GIT binary patch delta 1190 zcmZ8hO=w(I6rTI$&Ac15ah@}S4KdngtSE_(bMF70TC`vhTqz9!?IIm&5TqU2notpu zNx`8A+SEtAE?l%A7O@p#789f>U5nDL+_-QyZbU&Bu3UI#GRdTF-h=a<_ucQD@4K&a zt-pLC^2*Z6+p|q?m5|y>2-i)q z%AGcL&=NML9vOZDI#= zIeS-!TUVotkD{a&fcrEnYn)Twht@yQ6H{8!RDk0X3^?H=Lbzh0dOU#hwLj{7w|wvG zX#SI*^=T1 zKgLoe3@CxLUu}$4%I2#l?2Je2o%d#}YrsppHB!6K&!bvAXbJskK9!8>fqlk)D3KCW z;ofcjbX^*nd?zSUS~^I|eCGP#_Qhslt zROr7NS}(?5Rh*^uWn_XkU>zmz=%x5D3om_{Jhfj7w6Nz~(rplp%Ugd{V<0pfgkn7z zm3!<>{u_H(P$<0=84wxJrlk$P%$41gHC9-+*K9jKV4R3Rf5D_SjTTh~=bV%hgC#?W4 WLEe{mZP20C`Q3jT!yg2h@zH Date: Sat, 2 Nov 2024 19:27:48 +0100 Subject: [PATCH 010/324] Use ESM for rate limit; otherwise it wont work on my dev server (but locally?????) --- data/database.db | Bin 585728 -> 602112 bytes middleware/rateLimiter.js | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/data/database.db b/data/database.db index caa9fbb0d7b4b30bfb4b2936cbb1e31f04e600b1..ccc5cf7e392e87c9f58e2e912cedf258cbc1cd72 100644 GIT binary patch delta 2321 zcmZ9O-)|IE6vsQe^J@{>E<4*oTM*b51C+`A{iE0@Pxz)73Pc}ZLx`GywhF|Ei7m#) z5KziQt}kdXMuP!lX~Ggsgg%KN#wI|F8f9Na2>KUzW@fj$Yqxv1d%tI9&OPUQKev*)Lb_%v;-!plM`Hqs%nQP9u97In&6UM&1|5zjo?Trq`2%VM&=;J?<1ejwoi5 zo62=ZnDIxM$u1!%!y${uc&K*G_#;-gM2IOFs(6G!S6{s3Z?Y_5et*th;b5$Yh$fZ~ ztrQ$%#1+R{4zGE2?}1$h-dr<+rBaj=tj)wmKkyqdX4OTf=r_dZojdX*eawt+^C!)T zqgJOY5QhXzxyKt^K{XZ#zA4fwnydDgy;KQY+NVkjDYALTpwav*HS;A!hEZ9V}W9MP$8w)8xLWwcUuP{PjA?{* zD3DZ1Q>Yoe>gyuGQnuV%VoA7;Di;C=G9)<4Zz_7}Br$N$4d(t;zuUojgyGBAynUWE zX7+#YUCa}zC<%4)k&M;N&p0ENrOocU?$9!DoTi$H<|<%eigQzbkSV&UXy$T$Pd1+X zXOkt!!0GpSc8*LwE4*%CsCnBF4#Gp zCT8oo%ylDf`RjYA053Lk<6r^?EjS~x1s6&Uay0jR;4RE=bkCQ-CQvIDCiy{n?THD@ z6oQSBE%kHCz>o~J*p(0hULw-WJ@k8=L`}@sn#CSyE6{aUsJ7fd2b z%v7gULWD05_paf$)LA-DpzB4DoV zVL)db_n|E8p*}8QN1~P{Kq{09!IYUfWCc#5oB0ni0njs6%(d-yWjQPZe%fyPe-h;x zCQ=i|L!v0cO37;Nia!j9k1FpERENLum9>;g4nZnJ9B>{J2!QmhulguUIN0*x;*I~_MGibM{UfMx7g>p zA&iIMe>l?+ctNJ@$!eV(O(md@0H(_}G!UgkRQoIN8={$8=NGduYgXTM)GYeyYb#Hn z2jJxp%*wTbwwgcg`?_C&Tj20*Ax6v;c$`#G?MA@|79{~u<@La;D^dCH>*nbP{`w-| zyVZ&@R5&&B#)DX34X7y#QUN9Bmic*!J=qOg2F?N+0_PB Date: Tue, 5 Nov 2024 22:53:23 +0100 Subject: [PATCH 011/324] Telegram functionality and templating --- Dockerfile | 4 +- controllers/appriseController.js | 0 controllers/fetchData.js | 41 ++++++++++++++++- data/database.db | Bin 602112 -> 610304 bytes misc/entrypoint.sh | 0 package-lock.json | 17 +++++-- package.json | 9 ++++ routes/apprise/routes.js | 0 routes/frontendController/routes.js | 1 - utils/notifications/_test.js | 27 +++++++++++ utils/notifications/data/template.js | 61 +++++++++++++++++++++++++ utils/notifications/data/template.json | 3 ++ utils/notifications/mail.js | 26 +++++++++++ utils/notifications/telegram.js | 32 +++++++++++++ 14 files changed, 215 insertions(+), 6 deletions(-) create mode 100644 controllers/appriseController.js mode change 100644 => 100755 misc/entrypoint.sh create mode 100644 routes/apprise/routes.js create mode 100644 utils/notifications/_test.js create mode 100644 utils/notifications/data/template.js create mode 100644 utils/notifications/data/template.json create mode 100644 utils/notifications/mail.js create mode 100644 utils/notifications/telegram.js diff --git a/Dockerfile b/Dockerfile index 5fc294e..b23d93c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,7 +23,9 @@ WORKDIR /api COPY --from=builder /api . -RUN apk add --no-cache bash curl +RUN apk add --no-cache \ + bash \ + curl EXPOSE 7070 diff --git a/controllers/appriseController.js b/controllers/appriseController.js new file mode 100644 index 0000000..e69de29 diff --git a/controllers/fetchData.js b/controllers/fetchData.js index 43b4f8a..8df6b46 100644 --- a/controllers/fetchData.js +++ b/controllers/fetchData.js @@ -3,6 +3,7 @@ const { fetchAllContainers } = require("../utils/containerService"); const logger = require("./../utils/logger"); const path = require("path"); const fs = require("fs"); +const { exec } = require("child_process"); const fetchData = async () => { try { @@ -12,7 +13,6 @@ const fetchData = async () => { if (process.env.OFFLINE === "true") { logger.info("No new data inserted --- OFFLINE MODE"); } else { - // Insert data into the SQLite database db.run( `INSERT INTO data (info) VALUES (?)`, [JSON.stringify(data)], @@ -26,6 +26,45 @@ const fetchData = async () => { }, ); } + + const containerStatus = {}; + Object.keys(allContainerData).forEach((host) => { + containerStatus[host] = allContainerData[host].map((container) => ({ + name: container.name, + id: container.id, + state: container.state, + host: container.hostName, + })); + }); + + const filePath = path.resolve(__dirname, "../data/states.json"); + let previousState = {}; + + if (fs.existsSync(filePath)) { + previousState = JSON.parse(fs.readFileSync(filePath, "utf8")); + } + + if (JSON.stringify(previousState) !== JSON.stringify(containerStatus)) { + fs.writeFileSync(filePath, JSON.stringify(containerStatus, null, 2)); + logger.info(`Container states saved to ${filePath}`); + + //TODO: rewrite every notification service using custom js modules + exec( + path.resolve(__dirname, "../misc/apprise.ppy"), + (error, stdout, stderr) => { + if (error) { + logger.error("Error executing apprise.py:", error.message); + return; + } + if (stderr) { + logger.warn("apprise.py stderr:", stderr); + } + logger.info("apprise.py executed successfully:", stdout); + }, + ); + } else { + logger.info("No state change detected, apprise.py not triggered."); + } } catch (error) { logger.error("Error fetching data:", error.message); } diff --git a/data/database.db b/data/database.db index ccc5cf7e392e87c9f58e2e912cedf258cbc1cd72..682c1a7450519549c99fb503d233e1e184c7bbb2 100644 GIT binary patch delta 32321 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 delta 21844 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 l;?0=ISI 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 M;dr<_z^Etx521eIv;Y7A diff --git a/misc/entrypoint.sh b/misc/entrypoint.sh old mode 100644 new mode 100755 diff --git a/package-lock.json b/package-lock.json index 68d9374..1ee8b13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,13 @@ { "name": "dockstatapi", - "version": "1.0.0", + "version": "2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dockstatapi", - "version": "1.0.0", - "license": "ISC", + "version": "2", + "license": "BSD 3-Clause License", "dependencies": { "bcrypt": "^5.1.1", "child_process": "^1.0.2", @@ -15,7 +15,9 @@ "dockerode": "^4.0.2", "express": "^4.21.1", "express-rate-limit": "^7.4.1", + "js-yaml": "^4.1.0", "node-fetch": "^3.3.2", + "nodemailer": "^6.9.16", "python-shell": "^5.0.0", "sqlite3": "^5.1.7", "swagger-jsdoc": "^6.2.8", @@ -2566,6 +2568,15 @@ "node": ">= 10.12.0" } }, + "node_modules/nodemailer": { + "version": "6.9.16", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.16.tgz", + "integrity": "sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nodemon": { "version": "3.1.7", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz", diff --git a/package.json b/package.json index 1107785..c6f82f2 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,9 @@ "dockerode": "^4.0.2", "express": "^4.21.1", "express-rate-limit": "^7.4.1", + "js-yaml": "^4.1.0", "node-fetch": "^3.3.2", + "nodemailer": "^6.9.16", "python-shell": "^5.0.0", "sqlite3": "^5.1.7", "swagger-jsdoc": "^6.2.8", @@ -30,5 +32,12 @@ "devDependencies": { "dependency-cruiser": "^16.5.0", "nodemon": "^3.1.7" + }, + "nodemonConfig": { + "ignore": [ + "**/logs/**", + "**/data/**" + ], + "delay": 2500 } } diff --git a/routes/apprise/routes.js b/routes/apprise/routes.js new file mode 100644 index 0000000..e69de29 diff --git a/routes/frontendController/routes.js b/routes/frontendController/routes.js index ed4f127..de08c7a 100644 --- a/routes/frontendController/routes.js +++ b/routes/frontendController/routes.js @@ -1,6 +1,5 @@ const express = require("express"); const router = express.Router(); -const logger = require("../../utils/logger"); const { hideContainer, unhideContainer, diff --git a/utils/notifications/_test.js b/utils/notifications/_test.js new file mode 100644 index 0000000..71398c7 --- /dev/null +++ b/utils/notifications/_test.js @@ -0,0 +1,27 @@ +const logger = require("../../utils/logger"); + +const { telegramNotification } = require("./telegram"); + +async function testNotification(type, containerId) { + if (!containerId) { + console.error("Container ID is required."); + return; + } + + switch (type) { + case "telegram": + logger.debug("Testing Telegram notification..."); + await telegramNotification(containerId); + break; + default: + logger.error("Unknown notification type. Use 'email' or 'telegram'."); + } +} + +if (require.main === module) { + const [type, containerId] = process.argv.slice(2); + testNotification(type, containerId); + console.log(`Testing ${type}, with: ${containerId}`); +} + +module.exports = testNotification; diff --git a/utils/notifications/data/template.js b/utils/notifications/data/template.js new file mode 100644 index 0000000..2bec652 --- /dev/null +++ b/utils/notifications/data/template.js @@ -0,0 +1,61 @@ +const fs = require("fs"); +const path = require("path"); + +const templatePath = path.join(__dirname, "template.json"); +const containersPath = path.join(__dirname, "../../../data/states.json"); + +function getTemplate() { + try { + const data = fs.readFileSync(templatePath, "utf8"); + return JSON.parse(data); + } catch (error) { + console.error("Failed to load template:", error); + return null; + } +} + +function setTemplate(newTemplate) { + try { + fs.writeFileSync( + templatePath, + JSON.stringify(newTemplate, null, 2), + "utf8", + ); + console.log("Template updated successfully"); + } catch (error) { + console.error("Failed to update template:", error); + } +} + +function renderTemplate(containerId) { + const template = getTemplate(); + if (!template) return null; + + try { + const data = fs.readFileSync(containersPath, "utf8"); + const containers = JSON.parse(data); + + let containerData = null; + for (const host in containers) { + containerData = containers[host].find((c) => c.id === containerId); + if (containerData) break; + } + + if (!containerData) { + console.error(`Container with ID ${containerId} not found`); + return null; + } + + // Substitute placeholders in the template with container data + return Object.keys(containerData).reduce( + (text, key) => + text.replace(new RegExp(`{{${key}}}`, "g"), containerData[key]), + template.text, + ); + } catch (error) { + console.error("Failed to load containers:", error); + return null; + } +} + +module.exports = { getTemplate, setTemplate, renderTemplate }; diff --git a/utils/notifications/data/template.json b/utils/notifications/data/template.json new file mode 100644 index 0000000..daa1f49 --- /dev/null +++ b/utils/notifications/data/template.json @@ -0,0 +1,3 @@ +{ + "text": "{{name}} ({{id}}) on {{host}} is {{state}}." +} diff --git a/utils/notifications/mail.js b/utils/notifications/mail.js new file mode 100644 index 0000000..24accb3 --- /dev/null +++ b/utils/notifications/mail.js @@ -0,0 +1,26 @@ +const nodemailer = require("nodemailer"); + +const transporter = nodemailer.createTransport({ + host: process.env.SMTP_SERVER_HOST, + port: process.env.SMTP_SERVER_PORT, + secure: process.env.SMTP_USE_SSL, + auth: { + user: process.env.SMTP_USER, + pass: process.env.SMTP_PASSWORD, + }, +}); + +const mailOptions = { + from: "yourusername@email.com", + to: "yourfriend@email.com", + subject: "Sending Email using Node.js", + text: "That was easy!", +}; + +transporter.sendMail(mailOptions, function (error, info) { + if (error) { + console.log("Error:", error); + } else { + console.log("Email sent:", info.response); + } +}); diff --git a/utils/notifications/telegram.js b/utils/notifications/telegram.js new file mode 100644 index 0000000..5c79bdc --- /dev/null +++ b/utils/notifications/telegram.js @@ -0,0 +1,32 @@ +import fetch from "node-fetch"; +import logger from "../logger.js"; +import { renderTemplate } from "./data/template.js"; + +const telegram_bot_token = process.env.TELEGRAM_BOT_TOKEN; +const telegram_chat_id = process.env.TELEGRAM_CHAT_ID; + +export async function telegramNotification(containerId) { + const telegram_message = renderTemplate(containerId); + if (!telegram_message) { + logger.error("Failed to create notification message."); + return; + } + + try { + await fetch( + `https://api.telegram.org/bot${telegram_bot_token}/sendMessage`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + chat_id: telegram_chat_id, + text: telegram_message, + }), + }, + ); + } catch (error) { + logger.error("Error sending message:", error); + } +} From 2860402ccf38260d26583711fe9dd1a6743eefae Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 6 Nov 2024 00:52:06 +0100 Subject: [PATCH 012/324] More notification services and routes for testing and configuring the template --- config/loggerConfig.js | 21 ++- controllers/appriseController.js | 0 controllers/databaseMigration.js | 20 +++ controllers/scheduler.js | 21 ++- data/database.db | Bin 610304 -> 610304 bytes package-lock.json | 218 +++++++++++++++++++++++++ package.json | 1 + routes/apprise/routes.js | 0 routes/auth/routes.js | 3 +- routes/notifications/routes.js | 159 ++++++++++++++++++ server.js | 2 + utils/logger.js | 12 +- utils/notifications/_notify.js | 59 +++++++ utils/notifications/_test.js | 27 --- utils/notifications/data/template.js | 9 +- utils/notifications/data/template.json | 2 +- utils/notifications/discord.js | 27 +++ utils/notifications/email.js | 36 ++++ utils/notifications/mail.js | 26 --- utils/notifications/pushbullet.js | 30 ++++ utils/notifications/pushover.js | 30 ++++ utils/notifications/slack.js | 27 +++ utils/notifications/whatsapp.js | 29 ++++ 23 files changed, 678 insertions(+), 81 deletions(-) delete mode 100644 controllers/appriseController.js create mode 100644 controllers/databaseMigration.js delete mode 100644 routes/apprise/routes.js create mode 100644 routes/notifications/routes.js create mode 100644 utils/notifications/_notify.js delete mode 100644 utils/notifications/_test.js create mode 100644 utils/notifications/discord.js create mode 100644 utils/notifications/email.js delete mode 100644 utils/notifications/mail.js create mode 100644 utils/notifications/pushbullet.js create mode 100644 utils/notifications/pushover.js create mode 100644 utils/notifications/slack.js create mode 100644 utils/notifications/whatsapp.js diff --git a/config/loggerConfig.js b/config/loggerConfig.js index 0f7641a..38149ec 100644 --- a/config/loggerConfig.js +++ b/config/loggerConfig.js @@ -1,19 +1,18 @@ -const { format } = require("winston"); +const { createLogger, format, transports } = require("winston"); -module.exports = { +const logger = createLogger({ level: "info", format: format.combine( format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), format.printf( ({ timestamp, level, message }) => - `${timestamp} [${level.toUpperCase()}]: ${message}`, + `[${timestamp}] ${level.toUpperCase()}: ${message}`, ), ), - transports: { - console: true, - file: { - enabled: true, - filename: "logs/app.log", - }, - }, -}; + transports: [ + new transports.Console(), + new transports.File({ filename: "logs/app.log" }), + ], +}); + +module.exports = logger; diff --git a/controllers/appriseController.js b/controllers/appriseController.js deleted file mode 100644 index e69de29..0000000 diff --git a/controllers/databaseMigration.js b/controllers/databaseMigration.js new file mode 100644 index 0000000..263de07 --- /dev/null +++ b/controllers/databaseMigration.js @@ -0,0 +1,20 @@ +const db = require("../config/db"); +const logger = require("../utils/logger"); + +function clearOldEntries() { + const twentyFourHoursAgo = Date.now() - 24 * 60 * 60 * 1000; + + db.run( + `DELETE FROM data WHERE createdAt < ?`, + [twentyFourHoursAgo], + (err) => { + if (err) { + logger.error("Error deleting old entries:", err.message); + throw new Error("Database cleanup failed"); + } + logger.info("Old entries cleared successfully"); + }, + ); +} + +module.exports = clearOldEntries; diff --git a/controllers/scheduler.js b/controllers/scheduler.js index 5bb3ca7..6322d32 100644 --- a/controllers/scheduler.js +++ b/controllers/scheduler.js @@ -1,28 +1,41 @@ +// path: controllers/scheduler.js + const fetchData = require("./fetchData"); const logger = require("../utils/logger"); const db = require("../config/db"); -let fetchInterval = 5 * 60 * 1000; +let fetchInterval = 5 * 60 * 1000; // Fetch data every 5 minutes by default let intervalId; +let cleanupIntervalId; const scheduleFetch = () => { fetchData().then(() => { cleanupOldEntries(); }); + intervalId = setInterval(() => { logger.info( `Fetching data at interval of ${fetchInterval / 1000} seconds.`, ); - cleanupOldEntries(); fetchData(); }, fetchInterval); + + // Schedule cleanup every 24 hours (86400000 ms) + cleanupIntervalId = setInterval( + () => { + cleanupOldEntries(); + }, + 24 * 60 * 60 * 1000, + ); + logger.info(`Data fetching scheduled every ${fetchInterval / 1000} seconds.`); + logger.info("Old entries cleanup scheduled every 24 hours."); }; const setFetchInterval = (newInterval) => { if (intervalId) { clearInterval(intervalId); - logger.info(`Cleared existing fetch interval.`); + logger.info("Cleared existing fetch interval."); } fetchInterval = newInterval; scheduleFetch(); @@ -61,7 +74,7 @@ const cleanupOldEntries = async () => { ).toISOString(); try { await db.run("DELETE FROM data WHERE timestamp < ?", twentyFourHoursAgo); - logger.info(`Old entries cleared from the database.`); + logger.info("Old entries cleared from the database."); } catch (error) { logger.error(`Error clearing old entries: ${error.message}`); } diff --git a/data/database.db b/data/database.db index 682c1a7450519549c99fb503d233e1e184c7bbb2..d36f65d65fdc3cfd4736d30e65395b6a212c3393 100644 GIT binary patch delta 36079 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 delta 35527 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 diff --git a/package-lock.json b/package-lock.json index 1ee8b13..f4dcffb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "sqlite3": "^5.1.7", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1", + "twilio": "^5.3.5", "winston": "^3.15.0", "yamljs": "^0.3.0" }, @@ -470,6 +471,23 @@ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "license": "MIT" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -641,6 +659,12 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/buildcheck": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", @@ -882,6 +906,18 @@ "text-hex": "1.0.x" } }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", @@ -974,6 +1010,12 @@ "node": ">= 12" } }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", @@ -1031,6 +1073,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -1169,6 +1220,15 @@ "node": ">=6.0.0" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -1477,6 +1537,40 @@ "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", "license": "MIT" }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -2112,6 +2206,49 @@ "node": ">=6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "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.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -2134,18 +2271,60 @@ "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", "license": "MIT" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, "node_modules/lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", "license": "MIT" }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, "node_modules/lodash.mergewith": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", "license": "MIT" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/logform": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/logform/-/logform-2.6.1.tgz", @@ -2843,6 +3022,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", @@ -3069,6 +3254,12 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/scmp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/scmp/-/scmp-2.1.0.tgz", + "integrity": "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==", + "license": "BSD-3-Clause" + }, "node_modules/semver": { "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", @@ -3726,6 +3917,24 @@ "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", "license": "Unlicense" }, + "node_modules/twilio": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/twilio/-/twilio-5.3.5.tgz", + "integrity": "sha512-f/sA1Yd6TyIzfcq0u4QDGU+93afwswsJB+rf3T08tvBAMobBDVR3DfGREwJr5jp8xUic0qWa7GbJidk16NA4bg==", + "license": "MIT", + "dependencies": { + "axios": "^1.7.4", + "dayjs": "^1.11.9", + "https-proxy-agent": "^5.0.0", + "jsonwebtoken": "^9.0.2", + "qs": "^6.9.4", + "scmp": "^2.1.0", + "xmlbuilder": "^13.0.2" + }, + "engines": { + "node": ">=14.0" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -3911,6 +4120,15 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/xmlbuilder": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-13.0.2.tgz", + "integrity": "sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==", + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/package.json b/package.json index c6f82f2..b1c3b7c 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "sqlite3": "^5.1.7", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1", + "twilio": "^5.3.5", "winston": "^3.15.0", "yamljs": "^0.3.0" }, diff --git a/routes/apprise/routes.js b/routes/apprise/routes.js deleted file mode 100644 index e69de29..0000000 diff --git a/routes/auth/routes.js b/routes/auth/routes.js index a0b217e..bbc20d4 100644 --- a/routes/auth/routes.js +++ b/routes/auth/routes.js @@ -83,8 +83,9 @@ router.post("/enable", (req, res) => { const passwordData = { hash, salt }; fs.writeFile(passwordFile, JSON.stringify(passwordData), (err) => { - if (err) + if (err) { return res.status(500).json({ message: "Error saving password" }); + } setTrue(); res.json({ message: "Authentication enabled" }); }); diff --git a/routes/notifications/routes.js b/routes/notifications/routes.js new file mode 100644 index 0000000..592ab63 --- /dev/null +++ b/routes/notifications/routes.js @@ -0,0 +1,159 @@ +const express = require("express"); +const router = express.Router(); +const logger = require("../../utils/logger"); +const path = require("path"); +const fs = require("fs"); +const notify = require("../../utils/notifications/_notify"); +const dataTemplate = path.join( + __dirname, + "../../utils/notifications/data/template.json", +); +/** + * @swagger + * /notification-service/get-template: + * get: + * summary: Retrieve the notification template + * tags: [Notification Service] + * responses: + * 200: + * description: Template data retrieved successfully. + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates if the operation was successful + * data: + * type: object + * description: The template data in JSON format + * 500: + * description: Internal server error. + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * description: Error message + */ +router.get("/get-template", (req, res) => { + fs.readFile(dataTemplate, "utf-8", (error, data) => { + if (error) { + logger.error("Errored opening:", error); + return res.status(500).json({ message: `Error opening: ${error}` }); + } + res.json(JSON.parse(data)); + }); +}); + +/** + * @swagger + * /notification-service/set-template: + * post: + * summary: Update the notification template + * tags: [Notification Service] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * description: New template data to save + * responses: + * 200: + * description: Template updated successfully. + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * description: Success message + * 500: + * description: Internal server error. + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * description: Error message + */ +router.post("/set-template", (req, res) => { + const newData = req.body; + + fs.writeFile( + dataTemplate, + JSON.stringify(newData, null, 2), + "utf-8", + (error) => { + if (error) { + logger.error("Errored writing to file:", error); + return res + .status(500) + .json({ message: `Error writing to file: ${error}` }); + } + res.json({ message: "Template updated successfully." }); + }, + ); +}); + +/** + * @swagger + * /notification-service/test/{type}/{containerId}: + * post: + * summary: Send a test notification for a specific container + * tags: [Notification Service] + * parameters: + * - in: path + * name: type + * schema: + * type: string + * required: true + * description: Type of notification to test + * - in: path + * name: containerId + * schema: + * type: string + * required: true + * description: The ID of the container for the notification test + * responses: + * 200: + * description: Test notification sent successfully. + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * 500: + * description: Internal server error. + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + */ +router.post("/test/:type/:containerId", async (req, res) => { + const { type, containerId } = req.params; + try { + await notify(type, containerId); + res.json({ success: true, message: `Sent test notification to ${type}` }); + } catch (error) { + res.json({ success: false, message: `Errored: ${error}` }); + } +}); + +module.exports = router; diff --git a/server.js b/server.js index bf289f6..a62b625 100644 --- a/server.js +++ b/server.js @@ -11,6 +11,7 @@ const conf = require("./routes/setter/routes"); const auth = require("./routes/auth/routes"); const data = require("./routes/data/routes"); const frontend = require("./routes/frontendController/routes"); +const notificationService = require("./routes/notifications/routes"); // Middleware: const authMiddleware = require("./middleware/authMiddleware"); @@ -34,6 +35,7 @@ app.use("/conf", authMiddleware, limiter, conf); app.use("/auth", authMiddleware, limiter, auth); app.use("/data", authMiddleware, limiter, data); app.use("/frontend", authMiddleware, limiter, frontend); +app.use("/notification-service", authMiddleware, limiter, notificationService); app.listen(PORT, () => { logger.info(`Server is running on http://localhost:${PORT}`); diff --git a/utils/logger.js b/utils/logger.js index 853ca6f..9d25e5d 100644 --- a/utils/logger.js +++ b/utils/logger.js @@ -3,13 +3,11 @@ const loggerConfig = require("../config/loggerConfig"); const transports = [new winston.transports.Console()]; -if (loggerConfig.transports.file.enabled) { - transports.push( - new winston.transports.File({ - filename: loggerConfig.transports.file.filename, - }), - ); -} +transports.push( + new winston.transports.File({ + filename: "./logs/app.log", + }), +); const logger = winston.createLogger({ level: loggerConfig.level, diff --git a/utils/notifications/_notify.js b/utils/notifications/_notify.js new file mode 100644 index 0000000..b4a96fd --- /dev/null +++ b/utils/notifications/_notify.js @@ -0,0 +1,59 @@ +const logger = require("../../utils/logger"); + +const { telegramNotification } = require("./telegram"); +const { slackNotification } = require("./slack"); +const { discordNotification } = require("./discord"); +const { emailNotification } = require("./email"); +const { whatsappNotification } = require("./whatsapp"); +const { pushbulletNotification } = require("./pushbullet"); +const { pushoverNotification } = require("./pushover"); + +async function notify(type, containerId) { + if (!containerId) { + logger.error("Container ID is required."); + throw new Error("Container ID is required."); + } + + switch (type) { + case "telegram": + logger.debug("Testing Telegram notification..."); + await telegramNotification(containerId); + break; + case "slack": + logger.debug("Testing Slack notification..."); + await slackNotification(containerId); + break; + case "discord": + logger.debug("Testing Discord notification..."); + await discordNotification(containerId); + break; + case "email": + logger.debug("Testing Email notification..."); + await emailNotification(containerId); + break; + case "whatsapp": + logger.debug("Testing WhatsApp notification..."); + await whatsappNotification(containerId); + break; + case "pushbullet": + logger.debug("Testing Pushbullet notification..."); + await pushbulletNotification(containerId); + break; + case "pushover": + logger.debug("Testing Pushover notification..."); + await pushoverNotification(containerId); + break; + default: + const errorMsg = "Unknown notification type."; + logger.error(errorMsg); + throw new Error(errorMsg); + } +} + +if (require.main === module) { + const [type, containerId] = process.argv.slice(2); + notify(type, containerId); + console.log(`Testing ${type}, with: ${containerId}`); +} + +module.exports = notify; diff --git a/utils/notifications/_test.js b/utils/notifications/_test.js deleted file mode 100644 index 71398c7..0000000 --- a/utils/notifications/_test.js +++ /dev/null @@ -1,27 +0,0 @@ -const logger = require("../../utils/logger"); - -const { telegramNotification } = require("./telegram"); - -async function testNotification(type, containerId) { - if (!containerId) { - console.error("Container ID is required."); - return; - } - - switch (type) { - case "telegram": - logger.debug("Testing Telegram notification..."); - await telegramNotification(containerId); - break; - default: - logger.error("Unknown notification type. Use 'email' or 'telegram'."); - } -} - -if (require.main === module) { - const [type, containerId] = process.argv.slice(2); - testNotification(type, containerId); - console.log(`Testing ${type}, with: ${containerId}`); -} - -module.exports = testNotification; diff --git a/utils/notifications/data/template.js b/utils/notifications/data/template.js index 2bec652..9a090f6 100644 --- a/utils/notifications/data/template.js +++ b/utils/notifications/data/template.js @@ -1,5 +1,6 @@ const fs = require("fs"); const path = require("path"); +const logger = require("../../logger"); const templatePath = path.join(__dirname, "template.json"); const containersPath = path.join(__dirname, "../../../data/states.json"); @@ -21,9 +22,9 @@ function setTemplate(newTemplate) { JSON.stringify(newTemplate, null, 2), "utf8", ); - console.log("Template updated successfully"); + logger.log("Template updated successfully"); } catch (error) { - console.error("Failed to update template:", error); + logger.error("Failed to update template:", error); } } @@ -50,10 +51,10 @@ function renderTemplate(containerId) { return Object.keys(containerData).reduce( (text, key) => text.replace(new RegExp(`{{${key}}}`, "g"), containerData[key]), - template.text, + template.message, ); } catch (error) { - console.error("Failed to load containers:", error); + logger.error("Failed to load containers:", error); return null; } } diff --git a/utils/notifications/data/template.json b/utils/notifications/data/template.json index daa1f49..6a57d44 100644 --- a/utils/notifications/data/template.json +++ b/utils/notifications/data/template.json @@ -1,3 +1,3 @@ { "text": "{{name}} ({{id}}) on {{host}} is {{state}}." -} +} \ No newline at end of file diff --git a/utils/notifications/discord.js b/utils/notifications/discord.js new file mode 100644 index 0000000..c7bfe82 --- /dev/null +++ b/utils/notifications/discord.js @@ -0,0 +1,27 @@ +import fetch from "node-fetch"; +import logger from "../logger.js"; +import { renderTemplate } from "./data/template.js"; + +const discord_webhook_url = process.env.DISCORD_WEBHOOK_URL; + +export async function discordNotification(containerId) { + const discord_message = renderTemplate(containerId); + if (!discord_message) { + logger.error("Failed to create notification message."); + return; + } + + try { + await fetch(discord_webhook_url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + content: discord_message, + }), + }); + } catch (error) { + logger.error("Error sending Discord message:", error); + } +} diff --git a/utils/notifications/email.js b/utils/notifications/email.js new file mode 100644 index 0000000..d701679 --- /dev/null +++ b/utils/notifications/email.js @@ -0,0 +1,36 @@ +import nodemailer from "nodemailer"; +import logger from "../logger.js"; +import { renderTemplate } from "./data/template.js"; + +const email_sender = process.env.EMAIL_SENDER; +const email_recipient = process.env.EMAIL_RECIPIENT; +const email_password = process.env.EMAIL_PASSWORD; + +export async function emailNotification(containerId) { + const email_message = renderTemplate(containerId); + if (!email_message) { + logger.error("Failed to create notification message."); + return; + } + + const transporter = nodemailer.createTransport({ + service: "gmail", + auth: { + user: email_sender, + pass: email_password, + }, + }); + + const mailOptions = { + from: email_sender, + to: email_recipient, + subject: "Container Notification", + text: email_message, + }; + + try { + await transporter.sendMail(mailOptions); + } catch (error) { + logger.error("Error sending email:", error); + } +} diff --git a/utils/notifications/mail.js b/utils/notifications/mail.js deleted file mode 100644 index 24accb3..0000000 --- a/utils/notifications/mail.js +++ /dev/null @@ -1,26 +0,0 @@ -const nodemailer = require("nodemailer"); - -const transporter = nodemailer.createTransport({ - host: process.env.SMTP_SERVER_HOST, - port: process.env.SMTP_SERVER_PORT, - secure: process.env.SMTP_USE_SSL, - auth: { - user: process.env.SMTP_USER, - pass: process.env.SMTP_PASSWORD, - }, -}); - -const mailOptions = { - from: "yourusername@email.com", - to: "yourfriend@email.com", - subject: "Sending Email using Node.js", - text: "That was easy!", -}; - -transporter.sendMail(mailOptions, function (error, info) { - if (error) { - console.log("Error:", error); - } else { - console.log("Email sent:", info.response); - } -}); diff --git a/utils/notifications/pushbullet.js b/utils/notifications/pushbullet.js new file mode 100644 index 0000000..442f44d --- /dev/null +++ b/utils/notifications/pushbullet.js @@ -0,0 +1,30 @@ +import fetch from "node-fetch"; +import logger from "../logger.js"; +import { renderTemplate } from "./data/template.js"; + +const pushbullet_access_token = process.env.PUSHBULLET_ACCESS_TOKEN; + +export async function pushbulletNotification(containerId) { + const pushbullet_message = renderTemplate(containerId); + if (!pushbullet_message) { + logger.error("Failed to create notification message."); + return; + } + + try { + await fetch("https://api.pushbullet.com/v2/pushes", { + method: "POST", + headers: { + "Access-Token": pushbullet_access_token, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + type: "note", + title: "Container Notification", + body: pushbullet_message, + }), + }); + } catch (error) { + logger.error("Error sending Pushbullet message:", error); + } +} diff --git a/utils/notifications/pushover.js b/utils/notifications/pushover.js new file mode 100644 index 0000000..592e7f0 --- /dev/null +++ b/utils/notifications/pushover.js @@ -0,0 +1,30 @@ +import fetch from "node-fetch"; +import logger from "../logger.js"; +import { renderTemplate } from "./data/template.js"; + +const pushover_user_key = process.env.PUSHOVER_USER_KEY; +const pushover_api_token = process.env.PUSHOVER_API_TOKEN; + +export async function pushoverNotification(containerId) { + const pushover_message = renderTemplate(containerId); + if (!pushover_message) { + logger.error("Failed to create notification message."); + return; + } + + try { + await fetch("https://api.pushover.net/1/messages.json", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams({ + token: pushover_api_token, + user: pushover_user_key, + message: pushover_message, + }), + }); + } catch (error) { + logger.error("Error sending Pushover message:", error); + } +} diff --git a/utils/notifications/slack.js b/utils/notifications/slack.js new file mode 100644 index 0000000..2c1a67a --- /dev/null +++ b/utils/notifications/slack.js @@ -0,0 +1,27 @@ +import fetch from "node-fetch"; +import logger from "../logger.js"; +import { renderTemplate } from "./data/template.js"; + +const slack_webhook_url = process.env.SLACK_WEBHOOK_URL; + +export async function slackNotification(containerId) { + const slack_message = renderTemplate(containerId); + if (!slack_message) { + logger.error("Failed to create notification message."); + return; + } + + try { + await fetch(slack_webhook_url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + text: slack_message, + }), + }); + } catch (error) { + logger.error("Error sending Slack message:", error); + } +} diff --git a/utils/notifications/whatsapp.js b/utils/notifications/whatsapp.js new file mode 100644 index 0000000..d714b0b --- /dev/null +++ b/utils/notifications/whatsapp.js @@ -0,0 +1,29 @@ +import fetch from "node-fetch"; +import logger from "../logger.js"; +import { renderTemplate } from "./data/template.js"; + +const whatsapp_api_url = process.env.WHATSAPP_API_URL; // e.g., Twilio or other API service +const whatsapp_recipient = process.env.WHATSAPP_RECIPIENT; + +export async function whatsappNotification(containerId) { + const whatsapp_message = renderTemplate(containerId); + if (!whatsapp_message) { + logger.error("Failed to create notification message."); + return; + } + + try { + await fetch(whatsapp_api_url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + to: whatsapp_recipient, + body: whatsapp_message, + }), + }); + } catch (error) { + logger.error("Error sending WhatsApp message:", error); + } +} From b8f501e0fd425e777aeaf7ad07de073dcd31a66c Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 6 Nov 2024 01:14:45 +0100 Subject: [PATCH 013/324] Fix CodeQL --- controllers/fetchData.js | 18 ++---------------- controllers/frontendConfiguration.js | 3 +-- controllers/scheduler.js | 6 +----- data/database.db | Bin 610304 -> 610304 bytes server.js | 12 ++++++------ 5 files changed, 10 insertions(+), 29 deletions(-) diff --git a/controllers/fetchData.js b/controllers/fetchData.js index 8df6b46..ba14c34 100644 --- a/controllers/fetchData.js +++ b/controllers/fetchData.js @@ -47,23 +47,9 @@ const fetchData = async () => { if (JSON.stringify(previousState) !== JSON.stringify(containerStatus)) { fs.writeFileSync(filePath, JSON.stringify(containerStatus, null, 2)); logger.info(`Container states saved to ${filePath}`); - - //TODO: rewrite every notification service using custom js modules - exec( - path.resolve(__dirname, "../misc/apprise.ppy"), - (error, stdout, stderr) => { - if (error) { - logger.error("Error executing apprise.py:", error.message); - return; - } - if (stderr) { - logger.warn("apprise.py stderr:", stderr); - } - logger.info("apprise.py executed successfully:", stdout); - }, - ); + //TODO: logic + notification levels per service } else { - logger.info("No state change detected, apprise.py not triggered."); + logger.info("No state change detected, notifications not triggered."); } } catch (error) { logger.error("Error fetching data:", error.message); diff --git a/controllers/frontendConfiguration.js b/controllers/frontendConfiguration.js index 2ba90e8..cdbee13 100644 --- a/controllers/frontendConfiguration.js +++ b/controllers/frontendConfiguration.js @@ -2,9 +2,8 @@ const fs = require("fs"); const path = require("path"); const dataPath = path.join(__dirname, "../data/frontendConfiguration.json"); const logger = require("../utils/logger"); -const { PythonShellErrorWithLogs } = require("python-shell"); const expression = - "https?://(www.)?[-a-zA-Z0-9@:%._+~#=]{1,256}.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)"; + "https?://(www.)?[-a-zA-Z0-9@:%._+~#=]{1,256}.[a-zA-Z0-9()]{1,6}([-a-zA-Z0-9()@:%_+.~#?&//=]*)"; const regex = new RegExp(expression); /////////////////////////////////////////////////////////////// diff --git a/controllers/scheduler.js b/controllers/scheduler.js index 6322d32..e19b17e 100644 --- a/controllers/scheduler.js +++ b/controllers/scheduler.js @@ -1,12 +1,10 @@ -// path: controllers/scheduler.js - const fetchData = require("./fetchData"); const logger = require("../utils/logger"); const db = require("../config/db"); +const regex = /(\d{1,5})([smh])/g; let fetchInterval = 5 * 60 * 1000; // Fetch data every 5 minutes by default let intervalId; -let cleanupIntervalId; const scheduleFetch = () => { fetchData().then(() => { @@ -20,7 +18,6 @@ const scheduleFetch = () => { fetchData(); }, fetchInterval); - // Schedule cleanup every 24 hours (86400000 ms) cleanupIntervalId = setInterval( () => { cleanupOldEntries(); @@ -50,7 +47,6 @@ const parseInterval = (interval) => { }; let totalMilliseconds = 0; - const regex = /(\d+)([smh])/g; let match; while ((match = regex.exec(interval))) { diff --git a/data/database.db b/data/database.db index d36f65d65fdc3cfd4736d30e65395b6a212c3393..400a0e0e09b55b0717a56b00a295397aa0b270ba 100644 GIT binary patch delta 19067 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 JMCiqq{|}7$b8P?s diff --git a/server.js b/server.js index a62b625..3535097 100644 --- a/server.js +++ b/server.js @@ -30,12 +30,12 @@ swaggerDocs(app); scheduleFetch(); // Routes -app.use("/api", authMiddleware, limiter, api); -app.use("/conf", authMiddleware, limiter, conf); -app.use("/auth", authMiddleware, limiter, auth); -app.use("/data", authMiddleware, limiter, data); -app.use("/frontend", authMiddleware, limiter, frontend); -app.use("/notification-service", authMiddleware, limiter, notificationService); +app.use("/api", limiter, authMiddleware, api); +app.use("/conf", limiter, authMiddleware, conf); +app.use("/auth", limiter, authMiddleware, auth); +app.use("/data", limiter, authMiddleware, data); +app.use("/frontend", limiter, authMiddleware, frontend); +app.use("/notification-service", limiter, authMiddleware, notificationService); app.listen(PORT, () => { logger.info(`Server is running on http://localhost:${PORT}`); From 367bda08be3683126fbcca6a24d0e18e6e096d74 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 6 Nov 2024 01:34:47 +0100 Subject: [PATCH 014/324] Update mermaid diagramm for documentation purposes --- data/database.db | Bin 610304 -> 610304 bytes misc/dependencyGraphs/mermaid-all.txt | 124 +++++++++++------- misc/dependencyGraphs/mermaid-api.txt | 20 +-- misc/dependencyGraphs/mermaid-conf.txt | 16 ++- .../mermaid-notificationService.txt | 37 ++++++ 5 files changed, 137 insertions(+), 60 deletions(-) create mode 100644 misc/dependencyGraphs/mermaid-notificationService.txt diff --git a/data/database.db b/data/database.db index 400a0e0e09b55b0717a56b00a295397aa0b270ba..d916b79f6501db2292480baf31d83d3c2cb38163 100644 GIT binary patch delta 3699 zcmZ`+OKepc!ZIjqDAxWLfc|TI1%>wa~NFvfM2x{y_!~zn+S7)OEupd?MTtT(pt6WJNGWQukw_PG(Xax6M1o3{MNs+99gkKm_J-uBgU@X9%Ig9ORV=s0Fycxm8nfKk9GVuTn` zbKq`#|Kp7(j-5Gr<~tMnC{>0ED+G5}ZxzQP!-VASU?mZycN0$0##gap`7m1(VH-vuQDyVtn_!* zxI>52O>qu!H(yC^7P+ME^*_eriQ-&yPTkB$g~^Bg?L-UM(b-hs;96=X+M1(nmE)sSl?n(ip}iv+LX~h_c$vJX@G-PCu6n1;co>|I6~L zzqn_9n&^i(5iqMe5Q;D=g}}VKvnLoSArh@MjjN|dN^59i@j2x_^?vgGM)|S9kV{3} z$s=KHTvMcqAjXJJ7`R{*v1*)rvx4nbaW^|0jijrPcJ#wQy5~0(UW;)ct4w3&*~#Xo z2MfbN5s8Qp1e2R^uOyV(&dwSEhg+ti(YOV=rNLq)$)}lW%>3g~_xF#Za>VRDPJfk# z$d;cn@AT1nf8t3MrTH+Qm7 zuW3$<$^^Aq`fuw3ypCUSA*dpt+SrUSO_c(9;IJ-6b`$Ku#v}}COiC`41S3`cg|A~I z6#}_BdnBxGL(H_bLgVyZ)_`G332!eQ2uG6DMefe?;aETIfSzMjB?Z(a!&rUy98Pg6 zxNXp{O+Ml|MWuxsN#{|XS)BS5T2#eKX>EyHq6x^+27})gTf8eM)!27?TS#zAWBCEd z28sv?rgZkS#`Diucj!zq68Z&h@#W%(yOIWBvI@F8Un%bW$+u55-+nJngPO$YW#%ud zqu0fZKr@L84#!ou)t3HUH4L(qL4*)qo5liSTfe)2C^w*+t|G5=^ShdD7%Q?vyFabyP zgiRxk)Hd(+r@MlxMObkJn*GnTM$TdD#_YeUZu)Y3zl!3jA!a;N8g>yc4P6XHenJM^ z%#(3-Gs4SlcWFStQSe%p#Vm*O$`Iv%qeE%tmN$fR1uNXmcaus1YCVme#!(q9VbIG? z3UNgVDm`;crC=m&FK~+~{#(Q7fzq#QAh&^>WQyR4_||)1$cDAh>H1(WxrY*#cMOJM z5n7Hpj6T|1MU+G#fTy2Kt8U?MVK`1P0pfLz1_waJlrP&}Q%cxX=ZoA@R;=`Q!B7=+ zB6kaOgOWi$G70TNNmb3KX5+_#GD8w^DznvD(_B22ekQ2F4CK8wnIuAKW+44t5kN4l zl_+ya?p*V@FEc&ogRDU=kz|e8H}hJi^EIo-TSI;Mx#h@L9nUs3fjy> zB7y=I)P!GCcs*Z`*WflJn)JPRF(`x9R%F1rKQGbEJNKihU|12nGJk2kXmhB)@VTHY z4SdOLhT-361l~%_R!c2XvrEIlOsKInq_Kazl4=TV!V0&vJA|OOF<1X)+-U<6F13)_ zwlxg9cMQrk%$M@qb2zha6HvTcY3yYTARH%~P_d<;z=eBlEPcp*>1eul)XMHCK((kw z_vv?vuoEiU)u=G)b<59V6X@2_ba)U|pKU;12g&um1HGhg2=fXAxoY8!mCcnX2i*MK$cvRLV--k< z!&npu=s@$%g~SG7E<`)NobC(INUX_B4eb^(G()GM&=$7{)|KVxhfrW${hfH{m_*Cc zRTt1SK$^JQ|ND3(X6Qu~0)+k~X#@9;CznA=92Grc<}RK_oqFp;vS$l=WvF&{cofla zgYZ1@8k6IBa7ug?(r(9()8?2$#V{hXL@I^;7peyjpSy7|jmzP3U$P80{|;`h)<@vx zTF1>>Q3ws%Y2;X!b!d#xo=@M7e1T9#qd=kTwpjp=qbixaKqRX;JP}k-A3Wj~uO!-$ z-Ki@>tZ0T--?u*KJ$TFK*S0_Q*|xWv5cN{vUU(Bg2J+x%lts4p7=s@`Cbz}QI6>HYWt>2Xp!aPV&llt_PT*pX cIdY)%n+ooPq~f<4H4t(o;ocYDIGKL%Um?K6zyJUM delta 3213 zcmZ8jYlvM}8J%+D_XYSlbN@gaRSK@2txZjU4BJ(2@5j8<7QjsLkplB0kY?4-t zb|x0vLe(@E*;4;BAU+_WDXF;y1#|xy(`W|bFa6Q^C8A)W5lXGrwa+;-_e>eiIm21s zJ$vu9*IM6ReR<#N%lpoc@AN}MLqEhdh3nLdA25-B_2^GVk_SgF;l2*e z?`k^w_{i8}Pvh;8Z{zmpw{Uyp?9kOqdp|oGT{uUt(TH5HKY_Q;JiFs=gsXz9imQgJ z-rI3Ee(T+_nG?rP969~Ou`eB;S=5R$YCXGE-M5EwB?Y%m+xeN3$4?zO_0-HFV_Gq1 ztx*1=t~MWFTSb23%cM0tfYKoYb#Nu>@nw1zQ> zyu|naUGKyN6#crY{OIT6a}6hy)jD-n2*Vv`#(%0)HM^zeR!gVo;KR;1%c%1G%cMmL z-1k}1N(#jKd$;2G3s0Wx{mLXs)R2L`)F^>Lm1W%aHhx4Fq6QPx$`ph#3?R5+y}fV8 z2crhp&KPUc{Gpxor+=K-ae>XTh7TqJW)v3=<=vIb&BQRCmH73`N$Y_D=%YW!H>(ar z9PL!+CJYADTxwVPl8_vlQo{HDMA~tV`~EMnq%9;c)%)gu*Hhiqcg|Ee3me7CYXqNrvv<=KNP7S!xOs+cKmSEu$Z@$CN)l3F0?k#E`fHH@c8!66HuEl1sI$f|-HgMoWa>4>UreyR z0roRElWGW z_Rr#{XM-LsY@Ql8T{si$QQB-qrR}ax?%OR8e9GBvag7;krv0lwk6WaGLr}Nt#rxmZ zouB$d+-)Eu1Vbod zcxctVvPbVm%t?WCDp9K(xWN{!EYzDMLL-2r1ZT1qs*v<-^8|23P^m(#>962lCgu&p!_L3Qa-N5jzVc0&e@k=jM+sP)_ zUHhZ*%jaurqevlXQ_!2`DicK;gFY#y#xWX3K+G3yj62GWW)21o4~oPoZy6I(AZ@$r zXG0rv=yf2yG&`gL5(OKY=?>wW7j>`vzMc260$urJ{ITxxoeMPxn)$UG^|{Glc&Ec^ zODPvDEt%u~@QGS$c&j99jG5m9$UVCsWrqLGoVDn_Iz>y+2HCKE`qJQY!gX)I6GA{h ze^~fn_RdnyIpx_20X-}$@Jo+ZTcm*e?N3)0wp!RIe$H6Rw6G3J*RHqmR=g06ffK1= zc1vMz>@Qkd*1~4Ev<7*LUvMN)#$1V9Bb4^{4prv%pl3-c3_LfOT!;oM!4S_p8TwjQ z}e^Z z;D18}!T?E`H8zdZ{9vsWf*}Ci=xiPg4>|8_ zyulHS&((Bq=U2!})ec;sN*)WTU^vTqOpO14`g^;gW)>F#z(MBH7!x23QpYh&TO^7s z@kpAW80&#C`4kvC zSYixP1J6dJd8)qWq)9_}DLkOF#YhN}Cj@KirDj*hpwr`t|3M zTmId@LzzvKkA5xr T-uskb62}QJ_uZc^CvW~A!)8p4 diff --git a/misc/dependencyGraphs/mermaid-all.txt b/misc/dependencyGraphs/mermaid-all.txt index 0fc2d66..6036bdd 100644 --- a/misc/dependencyGraphs/mermaid-all.txt +++ b/misc/dependencyGraphs/mermaid-all.txt @@ -3,68 +3,104 @@ flowchart LR subgraph 0["config"] 1["db.js"] 2["swaggerConfig.js"] -9["dockerConfig.json"] +B["dockerConfig.json"] end subgraph 3["controllers"] 4["containerController.js"] -7["fetchData.js"] -A["frontendConfiguration.js"] -B["scheduler.js"] +7["databaseMigration.js"] +8["fetchData.js"] +C["frontendConfiguration.js"] +D["scheduler.js"] end subgraph 5["utils"] 6["dockerClient.js"] -8["containerService.js"] -O["extractHostData.js"] -P["writeOfflineLog.js"] +A["containerService.js"] +Q["extractHostData.js"] +R["writeOfflineLog.js"] +subgraph U["notifications"] +V["_notify.js"] +W["discord.js"] +subgraph X["data"] +Y["template.js"] end -subgraph C["middleware"] -D["authMiddleware.js"] -E["rateLimiter.js"] +Z["email.js"] +10["pushbullet.js"] +11["pushover.js"] +12["slack.js"] +13["telegram.js"] +14["whatsapp.js"] end -subgraph F["routes"] -subgraph G["auth"] -H["routes.js"] end -subgraph I["data"] +9["child_process"] +subgraph E["middleware"] +F["authMiddleware.js"] +G["rateLimiter.js"] +end +subgraph H["routes"] +subgraph I["auth"] J["routes.js"] end -subgraph K["frontendController"] +subgraph K["data"] L["routes.js"] end -subgraph M["getter"] +subgraph M["frontendController"] N["routes.js"] end -subgraph Q["setter"] -R["routes.js"] +subgraph O["getter"] +P["routes.js"] +end +subgraph S["notifications"] +T["routes.js"] +end +subgraph 15["setter"] +16["routes.js"] end end -S["server.js"] -subgraph T["swagger"] -U["swaggerDocs.js"] +17["server.js"] +subgraph 18["swagger"] +19["swaggerDocs.js"] end 4-->6 7-->1 -7-->8 +8-->1 +8-->A 8-->9 -8-->6 -B-->1 -B-->7 -J-->1 -L-->A -N-->9 -N-->B -N-->8 -N-->6 -N-->O -N-->P -R-->B -S-->B -S-->D -S-->E -S-->H -S-->J -S-->L -S-->N -S-->R -S-->U -U-->2 +A-->B +A-->6 +D-->1 +D-->8 +L-->1 +N-->C +P-->B +P-->D +P-->A +P-->6 +P-->Q +P-->R +T-->V +V-->W +V-->Z +V-->10 +V-->11 +V-->12 +V-->13 +V-->14 +W-->Y +Z-->Y +10-->Y +11-->Y +12-->Y +13-->Y +14-->Y +16-->D +17-->D +17-->F +17-->G +17-->J +17-->L +17-->N +17-->P +17-->T +17-->16 +17-->19 +19-->2 diff --git a/misc/dependencyGraphs/mermaid-api.txt b/misc/dependencyGraphs/mermaid-api.txt index c2dd6c8..0ae832b 100644 --- a/misc/dependencyGraphs/mermaid-api.txt +++ b/misc/dependencyGraphs/mermaid-api.txt @@ -13,21 +13,23 @@ subgraph 5["controllers"] 6["scheduler.js"] 8["fetchData.js"] end -subgraph 9["utils"] -A["containerService.js"] -B["dockerClient.js"] -C["extractHostData.js"] -D["writeOfflineLog.js"] +9["child_process"] +subgraph A["utils"] +B["containerService.js"] +C["dockerClient.js"] +D["extractHostData.js"] +E["writeOfflineLog.js"] end 2-->4 2-->6 -2-->A 2-->B 2-->C 2-->D +2-->E 6-->7 6-->8 8-->7 -8-->A -A-->4 -A-->B +8-->B +8-->9 +B-->4 +B-->C diff --git a/misc/dependencyGraphs/mermaid-conf.txt b/misc/dependencyGraphs/mermaid-conf.txt index 65e4b74..6e06cc6 100644 --- a/misc/dependencyGraphs/mermaid-conf.txt +++ b/misc/dependencyGraphs/mermaid-conf.txt @@ -11,16 +11,18 @@ subgraph 3["controllers"] end subgraph 5["config"] 6["db.js"] -A["dockerConfig.json"] +B["dockerConfig.json"] end -subgraph 8["utils"] -9["containerService.js"] -B["dockerClient.js"] +8["child_process"] +subgraph 9["utils"] +A["containerService.js"] +C["dockerClient.js"] end 2-->4 4-->6 4-->7 7-->6 -7-->9 -9-->A -9-->B +7-->A +7-->8 +A-->B +A-->C diff --git a/misc/dependencyGraphs/mermaid-notificationService.txt b/misc/dependencyGraphs/mermaid-notificationService.txt new file mode 100644 index 0000000..dbfbd46 --- /dev/null +++ b/misc/dependencyGraphs/mermaid-notificationService.txt @@ -0,0 +1,37 @@ +flowchart LR + +subgraph 0["routes"] +subgraph 1["notifications"] +2["routes.js"] +end +end +subgraph 3["utils"] +subgraph 4["notifications"] +5["_notify.js"] +6["discord.js"] +subgraph 7["data"] +8["template.js"] +end +9["email.js"] +A["pushbullet.js"] +B["pushover.js"] +C["slack.js"] +D["telegram.js"] +E["whatsapp.js"] +end +end +2-->5 +5-->6 +5-->9 +5-->A +5-->B +5-->C +5-->D +5-->E +6-->8 +9-->8 +A-->8 +B-->8 +C-->8 +D-->8 +E-->8 From 2d3af90ece7948ce7442675b4eecb5a3f4fa259c Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 6 Nov 2024 01:37:52 +0100 Subject: [PATCH 015/324] Default route rewrite --- data/database.db | Bin 610304 -> 610304 bytes server.js | 5 +++++ 2 files changed, 5 insertions(+) diff --git a/data/database.db b/data/database.db index d916b79f6501db2292480baf31d83d3c2cb38163..d9d2a74b9e61093b16a7f252a33a5461125c2bf9 100644 GIT binary patch delta 1475 zcmY*ZO=w(I6wZ4y^WK|~G%uN#K@-ztCJBr&dEEb>sL-8I#8wog?V=qqxUkhxQz|Mt zX+^h*uX>;bi6H7mFw{0s3T75A{3&GIy3p*Dx=5u;k@|DyrO!+j=gyh?op;YY-}%mM zu1+^srcEk~;6c|iSaIAMv@Qq3uzb_s7-WV}YvRt5nt=N6 z{zI;&&>r$?j!w^qi6{Klv?M|?78^}d$IezyEu1=j`jz@~3)Oi_rDej}q=TyW-Mf}R>@c#@UV`+Eulu!Zf&w~g(!;|`N5)PQ)+z{o`3L0~U-RImIDPZfiZ*rV- zSPGo6yhblsd%2VxKJwLomNJ%vGKOuznqgujn9o~1TTND8i=c7Ti=gsFAxbX%^?fM0 zR$9b@8il=;6i)w`m;13t*lNLS7wuRPL*)^|Q!aum23p75sJ9Efc`W#@AO*1&+IRd~ znNn?bi{*k*WSqg)j8iIVN|_QE0hLCf6l7rX(NPJn|LuS63JLT>Zw6X%E`sJh_u3%# z8+90W6qyQA!^7i&ElX)7mY8v>wct|GG>eB>5QddGC-UzCnBf7q@oyn4a52y6m7jfY zV%=-zp7H(urE;NX>kQN%$(LdCQ}3-wrnyl@#mGwPpgy_;M2X^f|3Cbx5sp$K7~FLc zs+BNE#oA}yu(yLk{e~aqJNvNQ2(Uljcz}&S<-`Q1lH&oK0v14*whs=3HqLI(omWWYWGTN*be^@Yuq9oU3W{c`e^am6q03- zwa%I@a!(CHa4HngIdAe_rciB4U_eHu<=7=lQjK~$aM}<*1Lz@d7B>6y&5Do&r`5_B z)8LQUOO@gV=twXNdgMv^ZuTDZ-inty68f(=|UH6z#t+SQce9q zG-(BYK(K8nN3u#G=%!jrpl?yd&QfgAY`V2wiZ0rXh`8`flSwAvh4UWV@4S1y`8+6@9q9q3kJg9lQ_*|dqWW;!Gcpb_cO~9p>}gHy-?1E2GBDn7~pcf zvYLAEM)=%XDZqQLP4UV+4^gTOc z;rc0fC5akC`)EKCjo9r%Xm}( zkN?0~nXsPJ#rq%df1}JSRVRhVYfU!Bb~o|d8?23g{K(U)oYm^a zJz zSbugR;PRjBuTc^Ql+KN#J}WT72K?d<4^2g_7%-6oqse34Z}{WY9@Mi>gYWKoe0Yal z9hEW&@+mnaI2k~J#BV#?kCB^ba1J!pHfR}e?U|&>Jxu|@*!?}ul3l>t^_{zIJpBv% cyh)=?bK-Jgi5%UzP5+*?kL$@-_1g3Q0XXzQj{pDw diff --git a/server.js b/server.js index 3535097..1c03ae9 100644 --- a/server.js +++ b/server.js @@ -37,6 +37,11 @@ app.use("/data", limiter, authMiddleware, data); app.use("/frontend", limiter, authMiddleware, frontend); app.use("/notification-service", limiter, authMiddleware, notificationService); +// Default route +router.get("/", (req, res) => { + res.redirect("/api-docs"); +}); + app.listen(PORT, () => { logger.info(`Server is running on http://localhost:${PORT}`); logger.info(`Swagger docs available at http://localhost:${PORT}/api-docs`); From 74e7af2ed33d9f19b0d80501dbdc77612ad835d9 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 6 Nov 2024 01:44:53 +0100 Subject: [PATCH 016/324] Hot-Fix missing import --- data/database.db | Bin 610304 -> 610304 bytes server.js | 1 + 2 files changed, 1 insertion(+) diff --git a/data/database.db b/data/database.db index d9d2a74b9e61093b16a7f252a33a5461125c2bf9..fdb4b87da60d1775b970169b9ed2cc8b0bcd51d4 100644 GIT binary patch delta 1315 zcmZ8hO=w(I6wZC~=G~W&ArogZp(Ztx`6)%GFZcf^6f_$nxY0$skU~0(AX-|mwneI7 zrid=Y(C{>;i!KsSvvN@iorQ|Y#+@(~L9|flx(HLSASmc*W~RxwI5WKa9qu{j`@VD6 zw(4tJ_3Jaan|@?kojuaouLB7bHg!tYkAbo=wU zb9n;S*d0dbZuTuaz52;`mtX`K8H^Ak+aB*mfBo>xh1E+dm)@%^a&Ej5ib)CECQ}&W zOyIX+LPGN)Ez&-mF1=WQ*I4GOvbBo)L~7-vv%FC`zj}V<^1ID9S1XHx3&-H~5~;Ue zx=S-;UI{O?O{{m^XfEAQHkVAo-nsC2`?H^EKx$lQAthspx6D~-of%H&nt9l}Nb7T! zD`th)R>(%>=^)-n#hkPTmUvJYeVPR2Kj}0bU>hpxWQEX9r_GbqqeggcoY4$>8{DKk zlU#bIfscnp+E0PbdN36au+7tAf1d2wIZ1F*824BKUEn0jXth9DYQ(qbDA5gj2c!GDe&B<{Ed2h`FENlM8eJj zc@8e#qc6+|!JX3jV3H?}Va9T!_Gdx*WiJPo8ew7V08Ss>`*u=Gg%ZfbB4HX0W#IeS z_+*KDsSd8h9BwVj@Whz0Hk9-6~*cjMNGE-6SINf;EfQSEP^I4@9rMk0AqjEdi z%3P)Na%(y}@?aU7%V7yRJJDKA3xlHiL}PCTIsBh#nKe$P&C}IqrQ_Bp<I41 z8Jc<&HSi3j98~er<66M2BwGvxqUqWnCE7bwFmK}qbaG*z`3+!vnq6JYNGDMmdAnx}%o<^tI{hR*=*rATlM@DG1+ hfe$?(MmgrCfo3UFojiDnY@>p&*#{qY>DS%(?0*(8VFmyI delta 1163 zcmY*ZO=w(Y6rDGjdEc{4%}nQ|L8ED!WI!Z7zI*TYI~AcDMclL?F09aI5nQy1peb1V zX`-OGYnaw6c2hxgrKUtOf>4{CprBdUb+QxOOhCkipx0!kojf=^xaYk4op&s0gavS}*@PcrK4%#L0Dsh zCbHcVZT0N`^VX4|h#|^G43Xi5Q(2h62sYXw7fyo-hN5?Tc3F+3xUXQC8_AChammx_ z=Mg!I=Qop?$2W@+RO$D-dd{Ws?YJ-nbdXPdPn_}j5$vd#X-uv zC#SVxxT5kDbP$%eKsD0?g13K9=lG9*(|VP3+uF^$DxIp_DXBXfjj3;yTKCJ<(#{J! z|6Jub|N3QmwFM5O4Hc3eS&oYs4y{H)NQB(jF3&tl&Re7qC3yoo;I+&%Fw`nd)#v%- zdR4sbGkh&qZJv5Q?KEV1lIOx;i0EC=p}YH$x-oK$0(xIqfGC?oG{N%HqH5<6hrcc_ zi2~x?@$y)eJVfNJbMlcLlzr?Z&wN!!>1}-h`!_JAvp(q_amG<3I}~2BvS Date: Fri, 20 Dec 2024 22:03:41 +0100 Subject: [PATCH 017/324] Switch to ES6 and TypeScript (#21) * Full ES6 support * Full ES6 support * TODO: fix npm run dev in ts * TODO: fix 'ERROR : Error fetching data: ' * Delete files * Adding more typing; making code more logical at some points; * Added typing and fixed > dockstatapi@2 dep > bash ./src/utils/createDependencyGraph.sh Route: frontend Route: auth ./routes/frontendController/routes.ts ./routes/auth/routes.ts Route: data ./routes/data/routes.ts Route: notificationService ./routes/notifications/routes.ts Route: api ./routes/getter/routes.ts Route: conf ./routes/setter/routes.ts ======== DONE ======== * Added typing and fixed 'npm run dep' * Advanced logging and fixing some bugs * Adjust workflows * New README and some other docer adjustments * First time building (god damn) * Fixing some typings * Fixing some typings in default notification modules * Needs fixing! * Needs fixing! * Create CodeQL.yml * Fixing more errors * Added some more typings and better /api/status route * New mermaid deiagrams * New mermaid deiagrams * HA route * No building errors! * Remove CodeQL * Use selfhosted runners * nerver mind, using too much ressources on my cloud machine * Added HA functionality (Please test) * Fixing package versions * Fix: Creating default config if it doesn't exist * Fix: Sync endpoint * Fix: Endpoint reachability check * Chore: Update notification service * CI: Added cloc * Fix: adjust cloc workflox * Fix: exclude node modules from cloc * Fix: we have no yaml files, probably due to some other actions or smth * Fix: exclude package-lock from cloc * Created new dependency graphs * Feat: Added playwright tests for API endpoint + Swagger test (auth) (#23) Co-authored-by: ItsNik * Chore: Cleanup * Fix: Added proxy support * Chore: Custom notifications * Feat: minified build Chore: Alpine based Dockerfile Chore: Advance Lifecycle scripts included in dockstatapi@2: start tsx src/server.ts available via `npm run-script`: start:build npx tsc && export NODE_NO_WARNINGS=1 && node dist/server.js dev nodemon dev:trace nodemon --trace-uncaught --trace-warnings dep bash ./src/utils/createDependencyGraph.sh dep:remove bash ./src/utils/removeUnusedDeps.sh && bash ./src/utils/createDependencyGraph.sh build npx tsc build:mini npx tsc && bash ./src/misc/minifyDist.sh --build-only mini bash ./src/misc/minifyDist.sh scripts * Feat: minified build Chore: Alpine based Dockerfile Chore: Advance Lifecycle scripts included in dockstatapi@2: start tsx src/server.ts available via `npm run-script`: start:build npx tsc && export NODE_NO_WARNINGS=1 && node dist/server.js dev nodemon dev:trace nodemon --trace-uncaught --trace-warnings dep bash ./src/utils/createDependencyGraph.sh dep:remove bash ./src/utils/removeUnusedDeps.sh && bash ./src/utils/createDependencyGraph.sh build npx tsc build:mini npx tsc && bash ./src/misc/minifyDist.sh --build-only mini bash ./src/misc/minifyDist.sh scripts * Better docker image * Chore: Update docker image to alpine base * Fix: use find instead of tree in minifyDist.sh * Chore: Update Readme * Fix: Force correct node version (took some time to find it) * Fix: Typo in package.json * Feat: added npmc * Fix: Remove data-bak * Feat: Switch to yarn in Dockerfile * Feat: Switch to yarn in Dockerfile * Fix: Yarn does not work => Change to npm * Fix: Specify Node version using nvmrc in workflow file * Test: Try npm i --verbose to see why workflow times out * Test: Try with all environment files * Warn: Removing arm/v7 support due to docker build incompatibilities * Chore: Add test build for debugging with dockstatapi: * Chore: Add opencontainer labels * Chore: Added automatic notifications Chore: Add init.ts instead of one big server.ts Fix: Allow usePassword.txt in ./src/data (previously not included) * Fix: Adjusted .gitignore * Chore: Changed from ? true : false to simpler syntax * Chore: Added lock file when a sync is running * Chore: Remove any typing highAvailability.ts Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Chore: Update src/utils/connectionChecker.ts Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Fix: Missing Typing for HA * Fix: Environment Varaible usage inside docker * Fix: Forgot the copying of the file inside the Dockerfile (bruh) --------- Co-authored-by: ItsNik Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- .dockerignore | 2 + .github/DockStat.png | Bin 0 -> 79885 bytes .github/workflows/anchore.yml | 36 +- .github/workflows/build-dev.yaml | 2 +- .github/workflows/build-image.yml | 2 +- .github/workflows/cloc.yaml | 28 + .github/workflows/test-build.yaml | 59 + .gitignore | 15 +- .npmrc | 1 + .nvmrc | 1 + Dockerfile | 93 +- README.md | 36 +- TODO.md | 12 + config/db.js | 19 - config/dockerConfig.json | 9 - config/loggerConfig.js | 18 - config/swaggerConfig.js | 29 - controllers/fetchData.js | 59 - data/database.db | Bin 610304 -> 0 bytes entrypoint.sh | 26 - environment.d.ts | 44 + middleware/authMiddleware.js | 50 - middleware/password.json | 1 - misc/dependencyGraphs/mermaid-all.txt | 106 - misc/dependencyGraphs/mermaid-api.txt | 35 - misc/dependencyGraphs/mermaid-conf.txt | 28 - .../mermaid-notificationService.txt | 37 - misc/entrypoint.sh | 26 - nodemon.json | 6 + package-lock.json | 3449 +++++++++++++---- package.json | 55 +- playwright.config.ts | 37 + routes/auth/routes.js | 146 - routes/data/routes.js | 111 - routes/setter/routes.js | 145 - server.js | 49 - .../.dependency-cruiser.cjs | 255 +- src/config/db.ts | 30 + src/config/hostsystem.ts | 61 + src/config/loggerConfig.ts | 45 + src/config/swaggerConfig.ts | 53 + .../controllers/containerController.ts | 27 +- .../controllers/databaseMigration.ts | 12 +- src/controllers/fetchData.ts | 80 + .../controllers/frontendConfiguration.ts | 91 +- src/controllers/highAvailability.ts | 274 ++ src/controllers/notificationController.ts | 62 + src/controllers/proxy.ts | 14 + .../controllers/scheduler.ts | 53 +- {middleware => src/data}/usePassword.txt | 0 src/init.ts | 47 + src/middleware/authMiddleware.ts | 52 + src/middleware/checkLock.ts | 19 + .../middleware/rateLimiter.ts | 0 src/misc/createEnvFile.sh | 34 + src/misc/dependencyGraphs/mermaid-all.txt | 106 + src/misc/dependencyGraphs/mermaid-api.txt | 32 + .../misc}/dependencyGraphs/mermaid-auth.txt | 2 +- src/misc/dependencyGraphs/mermaid-conf.txt | 24 + .../misc}/dependencyGraphs/mermaid-data.txt | 4 +- .../dependencyGraphs/mermaid-frontend.txt | 4 +- src/misc/dependencyGraphs/mermaid-ha.txt | 11 + .../mermaid-notificationService.txt | 35 + src/misc/entrypoint.sh | 30 + src/misc/minifyDist.sh | 38 + src/routes/auth/routes.ts | 174 + src/routes/data/routes.ts | 201 + .../routes/frontendController/routes.ts | 33 +- .../routes.js => src/routes/getter/routes.ts | 160 +- src/routes/highavailability/routes.ts | 92 + .../routes/notifications/routes.ts | 76 +- src/routes/setter/routes.ts | 180 + src/server.ts | 17 + src/utils/connectionChecker.ts | 77 + src/utils/containerService.ts | 134 + src/utils/createDependencyGraph.sh | 37 + src/utils/dockerClient.ts | 54 + src/utils/extractHostData.ts | 57 + utils/logger.js => src/utils/logger.ts | 8 +- src/utils/notifications/_notify.ts | 85 + .../utils/notifications/_template.ts | 42 +- src/utils/notifications/discord.ts | 55 + src/utils/notifications/email.ts | 46 + src/utils/notifications/pushbullet.ts | 59 + src/utils/notifications/pushover.ts | 56 + src/utils/notifications/slack.ts | 55 + src/utils/notifications/telegram.ts | 55 + src/utils/notifications/whatsapp.ts | 57 + src/utils/removeUnusedDeps.sh | 36 + src/utils/swaggerDocs.ts | 11 + src/utils/writeOfflineLog.ts | 26 + swagger/swaggerDocs.js | 10 - tests/main.spec.ts | 131 + tsconfig.json | 21 + utils/containerService.js | 63 - utils/createDependencyGraph.sh | 34 - utils/dockerClient.js | 45 - utils/extractHostData.js | 26 - utils/notifications/_notify.js | 59 - utils/notifications/data/template.json | 3 - utils/notifications/discord.js | 27 - utils/notifications/email.js | 36 - utils/notifications/pushbullet.js | 30 - utils/notifications/pushover.js | 30 - utils/notifications/slack.js | 27 - utils/notifications/telegram.js | 32 - utils/notifications/whatsapp.js | 29 - utils/writeOfflineLog.js | 31 - yarn.lock | 3298 ++++++++++++++++ 109 files changed, 9584 insertions(+), 2498 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/DockStat.png create mode 100644 .github/workflows/cloc.yaml create mode 100644 .github/workflows/test-build.yaml create mode 100644 .npmrc create mode 100644 .nvmrc create mode 100644 TODO.md delete mode 100644 config/db.js delete mode 100644 config/dockerConfig.json delete mode 100644 config/loggerConfig.js delete mode 100644 config/swaggerConfig.js delete mode 100644 controllers/fetchData.js delete mode 100644 data/database.db delete mode 100644 entrypoint.sh create mode 100644 environment.d.ts delete mode 100644 middleware/authMiddleware.js delete mode 100644 middleware/password.json delete mode 100644 misc/dependencyGraphs/mermaid-all.txt delete mode 100644 misc/dependencyGraphs/mermaid-api.txt delete mode 100644 misc/dependencyGraphs/mermaid-conf.txt delete mode 100644 misc/dependencyGraphs/mermaid-notificationService.txt delete mode 100755 misc/entrypoint.sh create mode 100644 nodemon.json create mode 100644 playwright.config.ts delete mode 100644 routes/auth/routes.js delete mode 100644 routes/data/routes.js delete mode 100644 routes/setter/routes.js delete mode 100644 server.js rename .dependency-cruiser.js => src/.dependency-cruiser.cjs (66%) create mode 100644 src/config/db.ts create mode 100644 src/config/hostsystem.ts create mode 100644 src/config/loggerConfig.ts create mode 100644 src/config/swaggerConfig.ts rename controllers/containerController.js => src/controllers/containerController.ts (58%) rename controllers/databaseMigration.js => src/controllers/databaseMigration.ts (55%) create mode 100644 src/controllers/fetchData.ts rename controllers/frontendConfiguration.js => src/controllers/frontendConfiguration.ts (73%) create mode 100644 src/controllers/highAvailability.ts create mode 100644 src/controllers/notificationController.ts create mode 100644 src/controllers/proxy.ts rename controllers/scheduler.js => src/controllers/scheduler.ts (56%) rename {middleware => src/data}/usePassword.txt (100%) create mode 100644 src/init.ts create mode 100644 src/middleware/authMiddleware.ts create mode 100644 src/middleware/checkLock.ts rename middleware/rateLimiter.js => src/middleware/rateLimiter.ts (100%) create mode 100644 src/misc/createEnvFile.sh create mode 100644 src/misc/dependencyGraphs/mermaid-all.txt create mode 100644 src/misc/dependencyGraphs/mermaid-api.txt rename {misc => src/misc}/dependencyGraphs/mermaid-auth.txt (80%) create mode 100644 src/misc/dependencyGraphs/mermaid-conf.txt rename {misc => src/misc}/dependencyGraphs/mermaid-data.txt (78%) rename {misc => src/misc}/dependencyGraphs/mermaid-frontend.txt (71%) create mode 100644 src/misc/dependencyGraphs/mermaid-ha.txt create mode 100644 src/misc/dependencyGraphs/mermaid-notificationService.txt create mode 100755 src/misc/entrypoint.sh create mode 100644 src/misc/minifyDist.sh create mode 100644 src/routes/auth/routes.ts create mode 100644 src/routes/data/routes.ts rename routes/frontendController/routes.js => src/routes/frontendController/routes.ts (97%) rename routes/getter/routes.js => src/routes/getter/routes.ts (68%) create mode 100644 src/routes/highavailability/routes.ts rename routes/notifications/routes.js => src/routes/notifications/routes.ts (73%) create mode 100644 src/routes/setter/routes.ts create mode 100644 src/server.ts create mode 100644 src/utils/connectionChecker.ts create mode 100644 src/utils/containerService.ts create mode 100755 src/utils/createDependencyGraph.sh create mode 100644 src/utils/dockerClient.ts create mode 100644 src/utils/extractHostData.ts rename utils/logger.js => src/utils/logger.ts (52%) create mode 100644 src/utils/notifications/_notify.ts rename utils/notifications/data/template.js => src/utils/notifications/_template.ts (51%) create mode 100644 src/utils/notifications/discord.ts create mode 100644 src/utils/notifications/email.ts create mode 100644 src/utils/notifications/pushbullet.ts create mode 100644 src/utils/notifications/pushover.ts create mode 100644 src/utils/notifications/slack.ts create mode 100644 src/utils/notifications/telegram.ts create mode 100644 src/utils/notifications/whatsapp.ts create mode 100755 src/utils/removeUnusedDeps.sh create mode 100644 src/utils/swaggerDocs.ts create mode 100644 src/utils/writeOfflineLog.ts delete mode 100644 swagger/swaggerDocs.js create mode 100644 tests/main.spec.ts create mode 100644 tsconfig.json delete mode 100644 utils/containerService.js delete mode 100755 utils/createDependencyGraph.sh delete mode 100644 utils/dockerClient.js delete mode 100644 utils/extractHostData.js delete mode 100644 utils/notifications/_notify.js delete mode 100644 utils/notifications/data/template.json delete mode 100644 utils/notifications/discord.js delete mode 100644 utils/notifications/email.js delete mode 100644 utils/notifications/pushbullet.js delete mode 100644 utils/notifications/pushover.js delete mode 100644 utils/notifications/slack.js delete mode 100644 utils/notifications/telegram.js delete mode 100644 utils/notifications/whatsapp.js delete mode 100644 utils/writeOfflineLog.js create mode 100644 yarn.lock diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..10b44ae --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +*.txt +*.md \ No newline at end of file diff --git a/.github/DockStat.png b/.github/DockStat.png new file mode 100644 index 0000000000000000000000000000000000000000..d375bd49107c79a960488d6062276a72cf6bd512 GIT binary patch literal 79885 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⪼EggaS+_T%ej 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 KU+70|um2a&GMD85 literal 0 HcmV?d00001 diff --git a/.github/workflows/anchore.yml b/.github/workflows/anchore.yml index 053123d..bb5df12 100644 --- a/.github/workflows/anchore.yml +++ b/.github/workflows/anchore.yml @@ -13,12 +13,12 @@ name: Anchore Grype vulnerability scan on: push: - branches: [ "main" ] + branches: ["main"] pull_request: # The branches below must be a subset of the branches above - branches: [ "main" ] + branches: ["main", "dev"] schedule: - - cron: '30 9 * * 1' + - cron: "30 9 * * 1" permissions: contents: read @@ -31,18 +31,18 @@ jobs: actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status runs-on: ubuntu-latest steps: - - name: Check out the code - uses: actions/checkout@v4 - - name: Build the Docker image - run: docker build . --file Dockerfile --tag localbuild/testimage:latest - - name: Run the Anchore Grype scan action - uses: anchore/scan-action@d5aa5b6cb9414b0c7771438046ff5bcfa2854ed7 - id: scan - with: - image: "localbuild/testimage:latest" - fail-build: true - severity-cutoff: critical - - name: Upload vulnerability report - uses: github/codeql-action/upload-sarif@v3 - with: - sarif_file: ${{ steps.scan.outputs.sarif }} + - name: Check out the code + uses: actions/checkout@v4 + - name: Build the Docker image + run: docker build . --file Dockerfile --tag localbuild/testimage:latest + - name: Run the Anchore Grype scan action + uses: anchore/scan-action@d5aa5b6cb9414b0c7771438046ff5bcfa2854ed7 + id: scan + with: + image: "localbuild/testimage:latest" + fail-build: true + severity-cutoff: critical + - name: Upload vulnerability report + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: ${{ steps.scan.outputs.sarif }} diff --git a/.github/workflows/build-dev.yaml b/.github/workflows/build-dev.yaml index 9833ede..afc8ffc 100644 --- a/.github/workflows/build-dev.yaml +++ b/.github/workflows/build-dev.yaml @@ -1,4 +1,4 @@ -name: Docker Image CI (dev) +name: Build dockstatapi:nightly on: push: diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index 8668f9b..4097b0b 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -1,4 +1,4 @@ -name: Docker Image CI +name: Buiod dockstatapi:latest on: release: diff --git a/.github/workflows/cloc.yaml b/.github/workflows/cloc.yaml new file mode 100644 index 0000000..9ce7e27 --- /dev/null +++ b/.github/workflows/cloc.yaml @@ -0,0 +1,28 @@ +name: Count Lines of Code + +permissions: + issues: write + pull-requests: write + +on: + push: + branches: [ main ] + pull_request: + branches: [ main, dev ] + +jobs: + cloc: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Count Lines of Code (cloc) + uses: djdefi/cloc-action@6 + with: + options: --md --report-file=cloc.md --exclude-dir=node_modules --exclude-lang=YAML,JSON --exclude-list-file=package-lock.json + + - name: Create comment from markdown file + uses: GrantBirki/comment@v2.1.0 + with: + file: cloc.md \ No newline at end of file diff --git a/.github/workflows/test-build.yaml b/.github/workflows/test-build.yaml new file mode 100644 index 0000000..fb47183 --- /dev/null +++ b/.github/workflows/test-build.yaml @@ -0,0 +1,59 @@ +name: Test building + +on: + pull_request: + branches: + - "dev" + +permissions: + packages: write + contents: read + +jobs: + build-main: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Set up Node.js using nvm + - name: Set up Node.js version from .nvmrc + run: | + curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash + export NVM_DIR="$HOME/.nvm" + [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" + nvm install + nvm use + node -v + npm -v + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Github Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Generate Docker tags + uses: docker/metadata-action@v5 + id: metadata + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=raw,enable=true,priority=200,prefix=,suffix=,value=${{ github.sha }} + + - name: Build and Push Docker Images + uses: docker/build-push-action@v5 + with: + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.metadata.outputs.tags }} + labels: ${{ steps.metadata.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.gitignore b/.gitignore index f7fcc52..43ddf88 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,13 @@ # custom paths: -data/* - +src/data/database.db +src/data/dockerConfig.json +src/data/highAvailability.json +src/data/states.json +src/data/user.conf +src/data/password.json +src/data/ha.lock + +.test* # Created by https://www.toptal.com/developers/gitignore/api/node ### Node ### # Logs @@ -141,3 +148,7 @@ dist # SvelteKit build / generate output .svelte-kit +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..4fd0219 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +engine-strict=true \ No newline at end of file diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..209e3ef --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +20 diff --git a/Dockerfile b/Dockerfile index b23d93c..87792b0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,34 +1,59 @@ -# Stage 1: Build stage -FROM node:latest AS builder - -LABEL maintainer="https://github.com/its4nik" -LABEL version="2" -LABEL description="API for DockStat" -LABEL license="BSD-3-Clause license " -LABEL repository="https://github.com/its4nik/dockstatapi" -LABEL documentation="https://github.com/its4nik/dockstatapi" - -WORKDIR /api - -COPY package*.json ./ - -RUN npm install --production - -COPY . . - -# Stage 2: Production stage -FROM node:alpine - -WORKDIR /api - -COPY --from=builder /api . - -RUN apk add --no-cache \ - bash \ - curl - -EXPOSE 7070 - -HEALTHCHECK CMD curl --fail http://localhost:7070/api/status || exit 1 - -ENTRYPOINT [ "bash", "misc/entrypoint.sh" ] +# Stage 1: Build stage +FROM node:alpine AS builder + +LABEL maintainer="https://github.com/its4nik" +LABEL version="2" +LABEL description="API for DockStat" +LABEL license="BSD-3-Clause license" +LABEL repository="https://github.com/its4nik/dockstatapi" +LABEL documentation="https://github.com/its4nik/dockstatapi" +LABEL org.opencontainers.image.description "The DockSatAPI is a free and OpenSource backend for gathering container statistics across hosts" +LABEL org.opencontainers.image.licenses="BSD-3-Clause license" +LABEL org.opencontainers.image.source="https://github.com/its4nik/dockstatapi" + +WORKDIR /build +ENV NODE_NO_WARNINGS=1 + +RUN apk update && \ + apk upgrade && \ + apk add bash + + +COPY tsconfig.json environment.d.ts package*.json tsconfig.json yarn.lock ./ +RUN npm install --verbose + +COPY ./src ./src +RUN npm run build:mini + +# Stage 2: main stage +FROM alpine AS main + +# Needed packages +RUN apk update && \ + apk upgrade && \ + apk add --update npm + +WORKDIR /build + +RUN mkdir -p /build/src/data + +COPY tsconfig.json environment.d.ts package*.json tsconfig.json yarn.lock ./ +RUN npm install --omit=dev --verbose + +COPY --from=builder /build/dist/* /build/src +COPY --from=builder /build/src/misc/entrypoint.sh /build/entrypoint.sh +COPY --from=builder /build/src/misc/createEnvFile.sh /build/createEnvFile.sh + +RUN node src/config/db.js + +# Stage 3: Production stage +FROM alpine AS production +ARG RUNNING_IN_DOCKER=true +RUN apk add --update bash nodejs + +WORKDIR /api + +COPY --from=main /build /api + +EXPOSE 9876 +ENTRYPOINT [ "bash", "./entrypoint.sh" ] diff --git a/README.md b/README.md index c12afae..ae34767 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,45 @@ # DockStatAPI v2 +![Dockstat Logo](.github/DockStat.png) This specific branch contains the currently WIP **DockStatAPI-v2**, this update will bring major breaking changes so please be careful. -With this new release a cupple of extra features (compared to v1) are going to be available. +With this new release a couple of extra features (compared to v1) are going to be available. ### Feature List: - Swagger API Documentation -- "Offline" mode (useful when working on the backend without available test docker sockets) - Database (Keeps data for 24 hours max) - Advanced authentication using hashes and salt +- Custom TypeScript/JavaScript notification modules! (Easy to add and configure!) +- `http` API to configure the backend +- Multi-arch docker builds (using buildx github action) +- Advanced security through middlewares: rate-limiting and authentication +- Multi Arch Docker builds through docker buildx +- High Availability using single master and ulimited worker nodes! # 🔗 DockStatAPI v2 Documentation + +_⚠️ = Deprecation warning_ + +- [Introduction](https://outline.itsnik.de/s/dockstat) + + - [DockstatAPI v2](https://outline.itsnik.de/s/dockstat/doc/dockstatapi-v2-XRMDKRqMIg) + + - [API reference](https://outline.itsnik.de/s/dockstat/doc/api-reference-1PTxqx1MQ6) + - [How dependency graphs are made](https://outline.itsnik.de/s/dockstat/doc/how-the-dependecy-graphs-are-made-svuZbEHH9g) + + - [DockStat v1](https://outline.itsnik.de/s/dockstat/doc/dockstat-v1-zVaFS4zROI) + + - [⚠️ Customisation](https://outline.itsnik.de/s/dockstat/doc/customization-PiBz4OpQIZ) + - [⚠️ Themes](https://outline.itsnik.de/s/dockstat/doc/themes-BFhN6ZBbYx) + - [⚠️ Installation](https://outline.itsnik.de/s/dockstat/doc/installation-DaO99bB86q) + + - [⚠️ DockStatAPI v1](https://outline.itsnik.de/s/dockstat/doc/dockstatapi-v1-jLcVCfPNmS) + - [⚠️ Integrations](https://outline.itsnik.de/s/dockstat/doc/integrations-Agq1oL6HxF) + - [⚠️ Backend API reference](https://outline.itsnik.de/s/dockstat/doc/backend-api-reference-YzcBbDvY33) + +# DockStat(APIs) goals + +DockStack tries to be a lightweigh and more "dashboard" like then [portainer](https://github.com/portainer/portainer), [cAdvisor](https://github.com/google/cadvisor), [dockge](https://github.com/louislam/dockge), ... +I also try to add some "extensions", like in V1 with [🥤cup](https://github.com/sergi0g/cup). +Everything is configured through a backend with Swagger documentation, so that you can follow the code and understand the new v2 frontend better! +DockStat is mainly used for teaching [myself](https://github.com/Its4Nik) more about TypeScript, APIs and backend development! diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..d2659e2 --- /dev/null +++ b/TODO.md @@ -0,0 +1,12 @@ +- [ ] Better Offline mode using "faker" library or self written (probably self written) +- [X] HA compatibility +- [X] !!! Needs testing !!! Add automatic notifications when container state changes, according to selected level for notification service +- [ ] Image update and update notifications +- [ ] trigger container restart / stop / start via backend routes +- [X] Add more logging +- [X] Structure code differently +- [X] Write new README and make the docs better +- [X] Update more files to correct TS syntax => remove "any" +- [ ] Websockets +- [X] Better /api/status endpoint with connection status of each host +- [X] Update notification service diff --git a/config/db.js b/config/db.js deleted file mode 100644 index 51850d3..0000000 --- a/config/db.js +++ /dev/null @@ -1,19 +0,0 @@ -const sqlite3 = require("sqlite3").verbose(); -const logger = require("./../utils/logger"); -const path = require("path"); -const dbPath = path.join(__dirname, "../data/database.db"); - -const db = new sqlite3.Database(dbPath, (err) => { - if (err) { - logger.error("Error opening database:", err.message); - } else { - db.run(`CREATE TABLE IF NOT EXISTS data ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - info TEXT NOT NULL, - timestamp DATETIME DEFAULT CURRENT_TIMESTAMP - )`); - logger.info("Database created / opened succesfully"); - } -}); - -module.exports = db; diff --git a/config/dockerConfig.json b/config/dockerConfig.json deleted file mode 100644 index 9ec4caf..0000000 --- a/config/dockerConfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "hosts": [ - { - "name": "Fin-2", - "url": "100.89.35.135", - "port": "2375" - } - ] -} diff --git a/config/loggerConfig.js b/config/loggerConfig.js deleted file mode 100644 index 38149ec..0000000 --- a/config/loggerConfig.js +++ /dev/null @@ -1,18 +0,0 @@ -const { createLogger, format, transports } = require("winston"); - -const logger = createLogger({ - level: "info", - format: format.combine( - format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), - format.printf( - ({ timestamp, level, message }) => - `[${timestamp}] ${level.toUpperCase()}: ${message}`, - ), - ), - transports: [ - new transports.Console(), - new transports.File({ filename: "logs/app.log" }), - ], -}); - -module.exports = logger; diff --git a/config/swaggerConfig.js b/config/swaggerConfig.js deleted file mode 100644 index 723897f..0000000 --- a/config/swaggerConfig.js +++ /dev/null @@ -1,29 +0,0 @@ -const options = { - definition: { - failOnErrors: true, - openapi: "3.0.0", - info: { - title: "DockStatAPI", - version: "2", - description: "An API used to query muliple docker hosts", - }, - components: { - securitySchemes: { - passwordAuth: { - type: "apiKey", - in: "header", - name: "x-password", - description: "Password required for authentication", - }, - }, - }, - security: [ - { - passwordAuth: [], - }, - ], - }, - apis: ["./routes/*/*.js"], -}; - -module.exports = options; diff --git a/controllers/fetchData.js b/controllers/fetchData.js deleted file mode 100644 index ba14c34..0000000 --- a/controllers/fetchData.js +++ /dev/null @@ -1,59 +0,0 @@ -const db = require("../config/db"); -const { fetchAllContainers } = require("../utils/containerService"); -const logger = require("./../utils/logger"); -const path = require("path"); -const fs = require("fs"); -const { exec } = require("child_process"); - -const fetchData = async () => { - try { - const allContainerData = await fetchAllContainers(); - const data = allContainerData; - - if (process.env.OFFLINE === "true") { - logger.info("No new data inserted --- OFFLINE MODE"); - } else { - db.run( - `INSERT INTO data (info) VALUES (?)`, - [JSON.stringify(data)], - function (error) { - if (error) { - logger.info("Error inserting data:", error.message); - console.error("Error inserting data:", error.message); - return; - } - logger.info(`Data inserted with ID: ${this.lastID}`); - }, - ); - } - - const containerStatus = {}; - Object.keys(allContainerData).forEach((host) => { - containerStatus[host] = allContainerData[host].map((container) => ({ - name: container.name, - id: container.id, - state: container.state, - host: container.hostName, - })); - }); - - const filePath = path.resolve(__dirname, "../data/states.json"); - let previousState = {}; - - if (fs.existsSync(filePath)) { - previousState = JSON.parse(fs.readFileSync(filePath, "utf8")); - } - - if (JSON.stringify(previousState) !== JSON.stringify(containerStatus)) { - fs.writeFileSync(filePath, JSON.stringify(containerStatus, null, 2)); - logger.info(`Container states saved to ${filePath}`); - //TODO: logic + notification levels per service - } else { - logger.info("No state change detected, notifications not triggered."); - } - } catch (error) { - logger.error("Error fetching data:", error.message); - } -}; - -module.exports = fetchData; diff --git a/data/database.db b/data/database.db deleted file mode 100644 index fdb4b87da60d1775b970169b9ed2cc8b0bcd51d4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 610304 zcmeFaOSC0fTHkjYx=0#9Cjy#>kf2Bb;?Wtz`;ith4HQzFt_GUIh?k3VPQ%aD;`%jx!_t%dfY@WWFTz%)&r#^M{bFaR7 zb#?WxdH$S7KK^?=pW^v6Pyb8)eCpS~z6u`y58pwnZ~E-Nzq zlhs>3^XgB0+pFr={rc4}e))~p{`PBMdG%Mn^2OJ`@Ri?s^_O4!tyjPB8*lu|7r#vB z*I)bc8=tp(JltQu`o?R2_l;M-{3|@a@ue@>??1i&pn39i^Wf2|zsSG)jW53b+N;0# z+An?KH@@`7tH1CYU-=4O?@jsSufOqy*MIf!g@5uDF#qn`eQW|9ty@y8S=i{_k)9cenr5?LWKyf&TBa|Lr&K zzi;?(@PC0vz$4%h@CbMWK1Kwd|HyB?a`hKKocz(9U%LPBZhYt7@BGmnIWXM0cc;Go z+SlG~?tK2v{hB{z<-REPX&PrymF7_ymTA(Y+c4ZWX_RM4Ri#B(-{GL~RKB0b?>&5Y|KVFS z+C6&j&G(*c-r}=al!i$f$3a!bWtoL#_W3&xng{*cQCLJ-l;rIv-@X6f{?j}6$|z2X zJT2tY51XfNKK|;Rdr26jaS1l$?46?A~b}-@X6f(e>k}8@^WE>^JYd`}EEqe_s8->^67z_up;a+C05}d^hTUjA6LV zvLwr!h96AcY{GKFv&JyzS;9*PSZ`ER&f-kWfjIvyv^&l&hlhaY|em~(UF&V z5Ep3{R%Mt}u?2C(*vZQDB*J-;l>@lpF{*6?7f>3WholUfyLI#6x`%FCS5*|{{N(q| zz9{p0C*!5cvaAd@alVPND#?l{iP9+ArTe0aHk)+Y)K$98<6?IXbo}GhU@Sl&&C~9a z6N53zvNYlU0j?-17@!q{TjL`GcWA5MF+=gt=X?Nr?6PE&ZNq(;RUow6?W4_pQ#47u zZ^~^^CdoE$cIB=NtE}EoTNp?E-@fT$12OS zk+C83aXzbH&I=6)WO<~Lrg$qRaETArS}CHqkkCZXC$ z_-3NCC=%99jg!n?)mpzL16TE$jp3Kyy8rO2zxw$4s~^0+dC30wxE+t2H*P%>I{lBH zeKW7`J^QBi1oG_J_wvC%c=kD7|Mc1S@cIv*eK)WF=-GGi`j4OeWnTa3v+v~fpFO+H z>%VyRSziC;Ga=dk{Monjdj0G#@%rG|xAFSUv(NB)|Jk?l`qr~syf)9ih1XlpYG3JO(IIT z`ftqr|JJ{~;(!0kBj6G62zUfM0v>_&An@U@J^N<3{;x0K`ak@kXP>6a4=m`iT!L_} zJRpwqEKJJ)GBq!&Bo1v-2d1nli>xEGCm7kJNRk+)yHgApdbLQ>ycFVFGdD&{Ff`V0 z*_~`or@pqS|1pBhZL<=2KrVD)kd;6bhgp%HsSZem9s?9Z+CsBw?P8r^g!KF)FB333rgsWDzQv;~6@%b#WW|*kf(YYJez*h+XlAJU``=B~kT#l?-UCl?Xk8K0@SaqYXj|ocOs>%Ys zwK9wIkVCo_o2Lar-Wf;y2*q$l>VvvYMp0Z8A%JSUjM)6pR=;Bc8;3sj`P27fO5A}kamIWE-U{sq^`u9b_~xz|ZMu_xW6O%&FfY!_|vVwaZuhwwbC zi#$%M{dT`Ck~$~(BPob<#VOkTe7hy=Rq-w<31^(25nFcu*88R9oTWXGs9p!s} zW12!QW!UBkSvPv@u?Qroj9U$t48B-3*YCB%@6c`e_*;Q^KaY|s49c?L%0-GwX@-ie?q#-kN`OsFsBVZ1FY%JG&T@MR#dD0{x3Yjvn z3%+5s80O(T{4%-m=5eVbL^1IXl6FS+6xG^=Fdll%#&Avdbw(S2+Iw-9lD?FYQ+9F~ zk~>k5g~IA^nAn`HUX}AzM79t)SBKBvs|xb$826IeqfwM0T4w2?y?##ysqSY(JZsB& zym`OhZtA8gs)F!GRc~ria<=??SX9$!8`aya3CS+mg%Se_w*@J7yB&#Q`@GuLO~y)k zR<2G`F`yiHW6~6>m|V=0@RForGI{v6O|(2EY+R5)bPQnM(gknmu^hTb_5SYD>$?x? z9zQ^!@h>Qxqorlw-0~Nn-@rqGBhH;!Q**<94?xizeDhw(rsm zTEdfr__8>NN%8_vd*vinnI@!!o!)yO+M<{wTwaIS&GgOkG$LO8r)PQ0tSPFP^htF*B7d9psN~pmJlU-dE}XjKax}Z0zaOP(-4wfgy93N> z$B#GL6x(gI*#TLTH4LG=h^m@ZG%vULJ_l-o%G;{iZnkyaoOR@^n9_L~Lf9mIu;ApRkuvA@p{;()oHYABHiUC_@VBlv zOvwSuD&6c;HXCx1VP5=uHU%OyO=w%BqH;@`Q6SOp4_Nwc(w!1fF* z6UlcDn0EYTUM(zRGF90UPcNLTMf$`(a?tErxaiSxId0dFnukvwHBJ2#2d92Q?_qaX z=NWuErfgO3xomJrteNL&%nYyDTKa9rUG+(8@MWQY*HIeM6$4C)s7^6_ibGqBh_a)pxt17Bw4q_(H;(74P?saaZ|-f zlN8(CF5d}#mZw`T%@r%qE)7{DH)R9m#;Ufha?UL{Z*bd7!f=;|XQ0X{Rgl9CgQRIL zLPa-~yLFN)A2OKJdr!_+NhAK{dVXhFSfn|VXJmxx4lcYHEDRl&QmLD}Fsg3LL42~?Jbt%%`t zS;kqhXRMGppAo@n~n3g9&2-8v$w8Jyd3S!LFU$pA{{*1|wNkeB2m!CZO zK7MOBF$TH$OK70oBY_Fk+C@?t;!SFvf&XviFaU?mzUi5t>65)EB1vBXlJrbiZhNx+Be$sK?X&FyBc;R9mQa9IEZj z0moiX_lw8$tc{lFIO-Erql72pD(=KQ9X~)Aop?2Bsgo<8aQ94BmQWVDe(f`CKm5P( zN!3eS8(vgB^qVrht)6Gh??Fj%X|^A65cmZ|)N%90AlJ!3df9kRoO#wj|K~{U0vw82O7n_J0QN@h`Tr%g2hcxc2{DKO zsTqU}(pzg(dI5ll+g@0?L1HpxQ0Ag2#il_tp=wH#p6mqxXYi|e7$d=pF)^neM2=;J zg=&!@_@9vzB6L0)tGrTskI?ve%<+CqsjEAs&30c^!&fj*e@slPvI5@Dr2VS|4B6&3r`6D-?nv!9{hjTXYR;5 z;{QbgAou@69i8@4PyC*`CS@BxSz0dV!k2o{7o}++FXorM)QX(n-|y$$Nzvbi+=>D! z@t}YshGcD8{(isYP?{kc2SO5|844rX602wh^OeG&{u;w8Z$>urhaA!b2Z^Fzy zz^H7wk|E3H+@&Ejmr?HbhYtEpSxI%r zZDRIw_V@CB5*S5FEj$tFjGR=gG3jsjlec>^AE+f6aNSHR@M9;j(DT>(^Yr~Cp=0(R z;-KCBW1&j;mzX;fxQ3F!Cfh>j`mu4bFTMr;3RaB z7zG2uZs8*-K+4fNih)@OABl#$34M-Y)qvY-t)g*?)9%{hn+WOro20wr z(Kxi#5X)E``pg}Q_WeJ`ScA?w_v(Bd|guc^q&xG9TC+l@A9j2?uazWaq+KhAe)uMscu=s0xiY z%IOtZ_@c^Ps zlEW>RFGLh>EneaQ$hXm1oJN%mG(>lzcr0P=vCx(=uTe9Q4z2ZDT|A)gW#hT-ct9?R zodE^hqe5&v1P3)ZJ>9=#DP{SLs{mFS%_r72M;p9P_n%<<5sMLc!YqO9rYbOevPYti z2b>oV;J}PCQ7*#y9AlVDHV-wyiwuEEFUFnNeB|iQh-RT2Hg}=z5w>tK9=W;d4(?KE zb+i}Dn-dV|ww6v8DaAj~Rh-_%d%oW)|1SCd8C%QohNGLiFdDwU?Y11m^#%ZZzW*$l zZiW{mEFLou4QWAcj5V*9-2ZVq(q(J7nsRUy4nPmM_P`GRZ`$g2y8D0EXYNR}@Bfhh zgaG0;|NqagZvDAL0{kzJfJeY1;1Tc$cmytnz=yy8SE&;4*T&QRS6L;1^qM|@raPX` z%~F3Es|45=8MHM;p3N}aLi_-oAu$e)e1^x2h@ug29Jve*ZQX<8&}Z(!QSmuljJG%4 z!U8kfhI{lY8OC}$%y0J_Dp|$2W^OT+PPTcoD|h0>Snsoj?-9?(CW`8?sqjz@<1@tc zHg?!w`>FsFAs!noOf3`1bUPiQC9x}(v|nqik@%Kkta*G(p$fpE`SEZ&bXz{&R!{}N zMi5bP1e@3e$|j;WG) zTr7}H7IyG!f$4t--&(&V2&!JQAzV`uKPxL-OVGR7ihI9` zC1#SQHKg`Wj7Q=4`#gStFuMomn{Smf&X_F%%_Hn8Bn!ZWR3rgd3t(h=10?|{fd$c^ z^9QzxK`#hcAP7haZzBk(P-l_5?(_dYg55u2XmMN)4#vy&EXVJkI?i)a+YG-iZXXHT zRzJ-RuTOL_AB%QdJ=~dY^|Y7IIjn)SSxJU~e49}f;D>^FHtdYMD`B%0&B(ztf`Aax zOlXH5Ul9aI!R;}7Ypa6`ukN@U-OCFC5W}&SA_;1DCwnbrJY!2eE9;_4$Yf&<43MG+ zz*7V_ID7Z$^Z@+I0Daa{UF5tJ2!~f&$2koP%=a3P(_r$qIGxiB>#)O z^z752ko>bkxI)}>^R|dr@r9Jy?fML8h{Ho${f^LHLh`qL=AI5c$&c@PE45$hd3F7@ zue~c(eog&0s#H^_joP8qWy_15IHX|ku?gz}ioDuy_uC?=Nk-pL@{*uHie`eUjurA> z-o={XBTFJ6`SH8$EpVppZxS~^K%o=kqX<@qYK+gW*72I7MF?tl5aQtNElSim5Tn`|E(w!TdcTx4DjKLP1Lwmd$0_DGcu@)v}h1*EZIDJ;|5XaV{j`b~496kma&>v_fmUyl~RKA7hiC}Xy8 zdUX{lpCh%@FCOqT6n-ZDD93PG9b7nd$K_~Vp8LmriP43pwNNe`7}*ijiV68&;$ksJ zX$sAcXF}?b|MDp(=q)1@E5W|C3#gF)T`wEYiDT3n$bTkjKlnQ3ymtUtvRoo|!Z|IX zY&~lw8Jc#0!t?!;e81Quz)Le*Nj0Zhwl#@8-G3h4FTTmh!~=E(od4LWZ9NaWH=gJ0 zA+lahoaTkTM{0&u%COxCZ7;RquunPOZ*&KDb+kI#>*Ji``6ra`H%7`DD%oRB^ZmX5 z^1?Wg=lccS8|?R=6WG8%mC4H;nOK+|^;pRNYlW`i^3A!@9gHpiFKzWZavmT0%$?37 z{C`}u`TxIvb?f&f65xM%1Uv#B0gr%3z$5T+An^Qif8~{{zqoaS7=M$75J(;ZX-A|c zbOu{KtzWX$CvVdcl|sTapztNX$*XmpUX>F{Gi*K?_Vy`IE-RuIXxqBy!=caI z^PyJ&+*kzwxw>J7t8h?}wI6fG>cR`%q(u^trg_-S`2bv_B!D6geP-4Kf+7h>D+$l=U7IXxL+=v|ADO&|wk~)>9~--gSoWnQ z0rl%aqaEG&`ZC7M%#`cT+<0zM)6aUvCKy&gXy_gM|cBq^%3i=gT?8^Se70wmP% z!rV$p`={1rzco+bDU5xe$KL~b$lvFD^X+oR=>{sFk1-cDrXLzk*=P7VAx7m?b+?Cc z$NV4q*;vmBEnb@W56NQ9(OwuEnG`vcwP}0me*yJRWm_r-2P}bAMB=ZLWbIV|OK7c* zkM1{cY67He?WcEewp>cpVK6Sy$$rKg)}JbNkhb>pm8os@(DVMl`u9E0nB41O{qr)) z*qVdf{Dwn~HP4y;6jGSs>tjg$2)@5mTfC6@9I(bIYmp7~9b9;I$K~i=UIkDX@9@Bw zhf``@(kC4iK#ny?$A3+1Q^M;@!BNI{KW5?cMO!!~rBtn5Kvgdr&xw=M8dLxUQlBKK zava%Xyx=Sty&ho6!ikq2+c^bLD$YH!eOp+1J-~?wMjXavP$BiAR7sl4>{AokD+10_ z1aK23924P**8Z|U#fZL~b2Wkrq(~SiEJk#{ND8eVqn4)f%Gky}ba2;6tE0VA&N;NN zQxAaM9_q*z+=j9iI`hxm@DM~dp zjfKn4@&7pa zuGj*YK64L^UKDU+MFAVL+?YljEp}K1$3=l1XZuu5UxoS5HVpTtFj0(>JP#} zS#XL`02MJMK6oOvsLSK$eDYa2qjdCsgjQfYu|LMa1RFvF|H7n~i_`lk7_s{gN!^VT z>V8{@2|XaHo-U9xu?Dhj=E83E<1d9QBpG@nT-djC$KKGx9(r@at&i&c-KW=gAJo0c z%%)DNZIo}LBHChIkS9ABMqxMsZO$;6 zNl0sh93agS5A~=F9j?!_`%6eF=whtCar`%flA+n{!u{6FRIDn9{F5`;koV~Itfcgo z?ni_=b~vZ!%{JZ@WfPVAsE)Typ6+%{6F290A~7orwn01Zd9`LfB1y`)_G5Tr;v2pn zL7G`?53^tRep!E6I5dQE__jLeH|6|o`2Mcra(FM#_hb260$dnSI2^TDY$pt(c}@{K z2r4HwAYt&a_pg*jMVR|M=6E;)>CoB*RQ0m)oH!J%fbYi=EH8tYk`(B6rFfAwI6dLN zWG#j6ufm|G`!l(vD9YLs{*#11s*@xSVjQ<9Do?d83p-EupGWuST&&sHf=+2B{yS=Q>y!|5m8b=pKT442QD z@_6tKZQU6?^qD*IM)?0UZ~6c0t6SHz*$Us#Bj6G62zUfM0v>^72t5Cm?}zEXe}kC* z3LSV75WgdWP9(i&7rH~YXZjZ~{p428C{IR8e?er8!ypGcb38;eo|4Z+U_&1QJEp&D ztKaFF{upKg9U{0`lgkG~ZV{SjHgX&EFT2APtYRS|DdF`N{C$65L8Vh4aDwgOaG{5TcA zTaB5S%!ju69huBS9~;bxAor!2e^U4vlLh|3kXw|tuw!5gn161-7WX0~BrJE~B%%v| zMAG}#E`qApYzWuH{0rR@MnRfZ(9T$V*u)EJfRx$+J;yZ>d(e6OQt|IQ<@jT%Ykym-K=e1AwUy$x!B}E#tMzRvJ`S@)A-KH#> zXeX6`-oAeFbP0!L+!7Fd!K;!4jr+f~+u-@1hZSq)jL>2j7A%6Xn5Q4MaB=PzE#O!Q zDUkyldbq2mkpuKS?R9ewYp^Og0H>Wq0B$P>=sLLg>W<6dy}TR%B?oGN0P6(vfz8K2aaSQbQ+F7h*px&KfGAXwjr%bx zygqgfa+1}c8G*&nTE8V*lj>!|IdM>0ff|6(SK>fp&fY=}+OyxQ0hX+!q9I8z`GC$w z040`rYMOg7z@!);LXeW-Z%vATSf#igDP9h6o*aPi4bt4CM2L@bIP||gzl$tOxDn3S z+tCA1BwWt^{W582{e&+tRxE|P(4m97Qd%ADwQ|nRz79P=cOLrq_Io!2%XRzV_O`o|VZ-oC(s+Rx%y{lWlw~WZ}tvv!B z0gr%3z$4%hcmV>>zyJF%|No8~#P&x}{8WUa-Y~a{3Xx(Lz43JaN%w#IHiFy#e7c{Q zFFZtlsWWsxMDFobM5F<2TlX|L^qG4a^mPA?rTd|-2-#yNNbo*|s&hOpDdY$hI75gm z1U~s6u(Nvb$<#+ha6I~S^W*V%=(c8^Sfw z`JBD zU~Gtgs_B-FKBXuAll+ise8Y%;$|;Z}9aP8~$QY!`fx`@VCxAQm?)=jIhj&jNbSQ3H zaX<_ZgEG_*RM;8{?*y=fuZp4N(bELDr$-@B+QJ@~gEC4@3E&gjJ;UQ;aZkW-SWH{~ zKEZrtb^+Ey3=pAa;zR7fdCOIKN`J$W< z5@)C%(a-h?x#Pg;_rr1E^7V+5`}ojSzawkG&}Z&Y9OM7vmj8eA>eibt;4u8BJpvv9 zkAO$OBj6EOioo;l{azFRpSeK_0InxQB=RH^lCcy`q7u;ibpIEp`->|sf!q|#@`}i~ z1srk!d=zFR7(wylXDFFO{ozok{kV`Dxjq8Q;c#fH-!b9aL!Y^aLoWxov2uWf>|fH? zf~YOP?j{dXj2(`@BWNeCB|`5f5M#C!1zMl4KN4=x&5wuQq1*EDchl7Xkz%u3W?%Si z4Uw6N0#;uB-?_0_JiB-@{y>{w}` z46z@N+D}Nff7e=rD-fz)_V}B?a7}UmsozbVPj~_3IV7ov_GW?}fZE7&N?qza{(e6h z^7lE+e2*MxM#w?wXqX-6JwaE#06E`9XH2Sw=$u&Mr3(VY82RUUU|xA)nyHcM^Zys* z|C2dhC78|>r0!Nyg}{Q+%K(;eS2?;G^c2`wr>Jjov4>H^5*a{*3(pLi+K~DY1py)z z?G`d$&ITsIU9XjGgc1E!-$TD?&XKyOy_C+H>+7ipV3RO;h(gkc%mn4BLy^$_1Yt`# zy7w!nVec9IqWjZ`37m%Z?>p!>1y0>@IhvQJ{c%00u=o!cFOshBnED9wic!*xYAQnH zv%BO9Qz7vAs`#u%rcaMrO>TAyw(Jqf0Bt}hAYu8^{Sh3*q?`*;y1}3;)AgSF|JIb4cRBYOYySQOir<;A0oZ-?%ST{uX zjEM*LL?Wy}p%3Yc_0`kyjE{-Gof7tQ=Z(rSddnvWy z+j|5&0v-X6fJeY1a0-Fv-|;z&|KGnsLIAsPu3JFUD*;}t1n7>^ooB)lCxE;x#A%!p z-NE=jh6=Jze{#MS>c4I4o(qj!!1S4WF7(v@jivrgv>ESENyrTI9Q}!XC0WG%XGJMQ z<1qLj@rnM7j6TEtcg>H--=W*`0qD8^8^`@ioQxvs_<)lN$PL^cfx-yj6L2!NAY?vy z|A|Tj6cQf=ntVY$#$@$Vt@T?HUr@d5;Wxnutcm)E2!uij+FVuAv8?$l#D9s|=V?53 z9)CZfg}3kX_YG{ES-w}!Eh7l?w^VUXw1ASS%PAjCcY#)gndyY0GZm|0%XG$DdyK ze}n`uIjkK0#5epus#K<}J)31MwR*Ty-Rfztr*l~u;h#2-?%%DO2iJY<7N>KJ<9A4y z_UXRC_<$(qb`$Q4Ew0mbn&+DsIO=$_jdw-aMCCrJ<870tyIs@7O?F1?mcI~o35ONJ z!}!)9u(BpT7VKw&WwNSdcz!j z@}!^X6zkm9Kj3g0qi*MqWu3NOxoMCiSr8oA7j08e0miAzisOdy`j%s+>ZGF z=w6n#`Tq~EZb>yj|H~ub5%36j1Uv#Bfo}i;&u@JXDu8!xkP0A|VsOwAeN8yq=-I0P z7N`K`8vqcKwTRe=ddp*hFi`=tZQawMr~ukNb5Dm}1#n|k0Fo?BDtRCw)S@ED&;=7Z z0Sd{3^Bn+CzQd3W7h}AGBdV?EBk=`M0rbs}$KRpb@&UL;6+lRo<#f={c>J3H58riX z*+><2lI4!Cc0*eiJaG+Z``Fk`#IP^!01yLERmId-hJGsHNURvw0tWyRGqJN62A~l7 z;ddpcE`NFMU{IpBQ19CAit z+wlJkc%o?zb0886`iJN`Ak5B**<3jPKPLH${zVl{}=H86bVjXRK*va zL^szm80i>F7TGsWaevURu`H1N_~hh*@MI(yMys}^JKGOE?72RNu#z?%Mcvab(XEE%QEmlKhfhYiF;K@--f`5#LFjsdTczm>OB*h({n^bETP}R%EbK>N*0`{L( zg&05O!R0sk-#R-n@0o^k47>(LpYOgseZF+;zg&CEG?16>vB30x0F!FuT`^d;ZAUnmWckD0*8sZ%$1VN~6ggXsDHmzV#iLLzHxKl+5r z=l_wzj`06&TX*OUeddn5BmO_UmzLiD{~JK__%G=Z@CbMWJOVE}0?)tqyKk8PKhkfV zfOrtpcA+xQ_%C4mIl0Y<+rZ+J`=$M$VDH5OdL&#sv~|yeL!Y_lLC^T#SjHcE#1ykV zhQXKg?J~9rf08*#lRFM5=;p`6PjWy_x8>t+1%y9YQq*6k=!v9Z zN*1PtC~lBAp7+VZ6X}6M>f_9zA`3$1htLnla1L!}ejL@PRKiPM``&pYOjU-#;T^9I7AEkIFsd`#MAm zCAdhIopE-|#iv&40qdbDgg*)N6dxX0G_4-)5Vd+PAEio6acRPzMAiZcisS){;j*Li zA>BVqW{FdnMR6V?=~G7};q7s{$z+??2%5BN-9f)8E2-|d9No(k{uNrE6u)qm8j}4H zidt(aJAa;uxa{OM4~36`44c@noI>5lti$ppHEIUZp|yTX09CzgJSR>{Dp%hIw__}C?r~4=AeyHa-rwoN?IjKbv+TeM* z|2(=MT`nFRc^KrVbTc_LS<571DQwD5U(TWakQE{{RAOkM?GXav2IA;)Wu4R=+=bHW zXfKs>j-M;#`*W(Go!)Ia0-vG@!*(OYJr{yvL^h$DyD+M5%R%&fKZEop=le73h^$eS zkCTdgoZT>&MmzDRTsOA5^ zbam@XFFRG^zkiQ_N5CWC5%36{hrsin{4U7;pSeld{UJF6L0L(vI1xjUzp6-t=lWmF z^`qrRB~J-aax_aU2gOWGLC4jCl3Fv&$RPAd%(82)N94VXDUpY^`j{w(K_UO!K64L) zt0Mm~XBAg#5?+bga;)u{!Y+!FLZPB@KYe9Jx?+A1fMCwmEx)h9GV#Q&mHxbdcw^N7 z_;nTq;jaKXii(cnrKW}=K9&T-rGBk^a)RL!dY`7oKA=aC^naXjq0Nr3Cc3!`rRuhP zK(0Uy(1F#DKWZ?}PXDK+#r_uraO5YVA(Bxj?0?%! zX(?$Nl5vJ&-M$(xp#QP5gasG^HDVe|bW-lx8Ye*zu|VHizh$QK&}%k^Yoh-tqfnAC zBrn}#O=+k-Mj1)HC+D=z6-{ScNo}c4+jpyfi{x;7q@_do!mw0}e=kM^m&hsDf{0fgmM}Cdx%RIl% z^Q%0+!SfZK-{g6N=eKzNW1ipP`MW&7$Mf4fZ}R*u&xYqe;o0$s+u|Sayvy^1=Mm3) zJdb(a=XuKW0nZ=u{1MMrdH$G3enwyC`A>PC@%#zTKj8V#c>a{<@ALdap8uTZAMyMb zJpY*IzvTI+JpUEXKjZl)JpY2{&v^bN&p+q+bDsa2=bGn7cpmWlE1q|F{vOYLp0|14 z;@R^wJo4}UG|w%HR1z4YaJ}RXT7%UK0~YaFc%8uK;{`5mw8WcjEO7$h{Tanth2}4i zeZ-lf4-CD}sgx4J$IGeJ!(C9Vp7xSDhd)@6Fo4{BISrKoC6gGhp?J-x(-NAA2uWn8 z$Cx@TLG(w3q#0{b5Xu0KV4Pr&@f9n8LkAaMQ3muKm&1E`WdJ}PN-_7RCVWA#qMzsf zkMzv&Wc?lmu^$y7XU`EFrP4s#+C@?IvO%0UI;}tsKqNn|SUyn#NMQ^NCafXp1p!MI z&;nTr3Kg_|*nCtd)hsN%AYf7ufSMw&g1&G9LO}~WF9do%NmLpbTw9g1DNF5VN8OWt=KZhdBXvN0>NxJ>dHQ_GZ!~I^Y zo^!t)6~TVIdB4}i*A3#zqS-W6jYNKzZ3*3_8;;l8sNQBx$a1+8x34VRqJj^1J4&nV z^J-f+*(Tp+(K)(!3|6tIqyT0iSF{{y2%W^dI zSPtSUG7R{!xYr151j#p;lRShyAmK-Cd^cev*tT>5y!l3gc%~&4icV;$wgQ%Yg5zAA zWW|gEq6NzUd{cn}aTt;4zDbzjyfSv&uzDN;t9vdVoGXeSV9kr9vj`6L#VmkxXt-Gg zTB!pCN5~IDLIwQ_ing9C#E9GN@d9dfaN*S*m!rFa@BtMSh5e%>D@j#_s9RM`1}8y) zA&s*A>_ku|B@V#_P%AyFGR{b_mUE#-O_ovBTE8VLsp@6pxtd5q^VR!L8%YP-J$kR- zz`M;uzBjdxjC|+Lr{p_sJ1k7@TS2)$tj$Bhr5PaY}>^bUvQ8B%poKxE3jlPZ>+>#1TpauDp> zx;W}S_LQ4*A??~cdVKxW5AN>oKfEgkIg=<^M7un02(C6svEA+RU9pE`$U$>8Y34v;zY=y+9pG>MWwhR!{!7XkF?#C zKNl{w-yp?>g@Lgp&ajxl)B?0eTR7s#65Pl#HDLEXVbP zYk~qcNr?=iF&P`Mz!k+iFJ8!Fz0T)aoJbtU-Jwmkyw&}S}g>i(a<8ix0x z@Lm$i{eR@|rEUOL>ls_C{j)S@qk?(J^4N*i79JTc?ElZv9kCvzSh22;F}bxiJa7NM z!2UnSoFYww4CNV;9jBfCBMLrw#xsgFb~3;E&2V<-geg`H8@|ifwwo zC<}8hu{sl1(Tj*I%T^Elrp$Az=iEg`MdgOd3YRtmU^Hd~MFPZW6s$)NX#p&T7_4Wf z?+C)+mqbsHXbkuQctk)X9t(O}9b9;I$K~i={?0DJ&Qjb!(1(&3Cnr)hYB2*KPQ=h_ zKRa<}=iWwuJC%!DKW6igzETd}-h9kKVrcCGs(RUYuIA4E_`Qb@?>~IYzO++Tgp%us zkEt%g=Hxh4K>k-!VBs{3-u)jwjyM2CRPfu1xFxVJ*%w;R{!g<1SU(av3#i+J_df;Y zDmx74>HqV>0kS9KOdq89LP=e>$liX4_LfxeJ2}C{3Y-8}tQX3R&5T$B5#y17mG0oK zl~zZ4wVV@fSw}peyJ~gpC3kVID}AtKV#8?(eJZNZzu$3$$VU+&K5H5(6Zhi^MuLuM zuHTa*lIpe`#FgIv@=P|9|S%?_Tl0|K$AU^=P`c*}cRiS8#Q}lpVP{$hryx0)HzKo#lA$euSWdx0tl+0FF z5pwx)haF=ghqmrvaOgAlFnAoN?FsWN*<{;r&y6R8JKs^@cE2f_B;GgWmRNtX&6{1h z6Z`pkpEX&L@5``>qB?A*TK^1_6~BsRfeM&rgN`Pk*%&EB}^p(3lCHLtFihpgHuh!JLR=U)%tIH7lbY zxIjqRNlIi}V{(B30CE3VY#Dn1F#v#y#=iY{L}CEYw{{^^y=G&$CIbLO7zo!&$0Tz(TR zjbcwZ0!mHw8Jx6X@JrmAWr^r#xMA)&LNelyS52#jyKY)N&zS1#;ruBZ13?{BNFvAp z8sYp!=Lu#fRG5ashvzm7O^+3X^Jk9$wG5#?en|M;+5{mw z{U}D5*&XL6C>Y`VIb_<>Rv~Rk`ad4AsMOXZw1{5lB#oZL7PZH>oN zOroQ8W`h4uXKg+$|Ig`2-7;GupPRxls=B$0s_M2JQ_uf%wN#pcj)xZC+(9s!SlN5CWC5%36{hrsh6 z{b5M|AG<-MKaLjEbQD)bNHCJ3ty8t9{J$ZTKZ!Bm%*oC8)C+x24Opb3`jlr#LVh-F zTm4Q;x%PeLp8q`Me`6i~xgC(fm%(8XbFSe1tvL$gPtIS@5l)3&3HG3IC;F(z9*jSp zDKB9B`{u{P@6c`e_*(=%-`6ybA`g*v2p;JiWM@xb85+B&dBdJx7B|^8sPvC2)dpyHAiVyf2%AO=Oab?g5%64RR z&~M6yq&qH0_wrmnR15bKZ0Y-7)~KED$tUc+l%i1j6k+Z8FB>@{sn#x_s+W!D#3^Y7 zTtAkOc&g`eSc8=J%#p;_jKLd z#Z`4%j;g2o8JRbk?l%@alH!0-1V)D(HtqEjJ>-1;-_$r|5Sf#>aQOuFMn#d6G$32a@cR>!z%=tep`b%4v zW+3_gSg)Usf4`*p{x~ed2~yM&pVd6$BWuzM`JWy#H>RT7LjG6OcT^DuN%w~q6Te_R z=1go`HP`R8+LJ@KX}V*pia z7eUo)HiT=U`l+vgk*Bb$QA8}EXQmFx{X`K93_eGM3w{Dr^CJI( z!re~ATQ3=yPtB0^CPlylp7xhRFOYAIXV8pcO&vdKPY~^8G_^$aL><4jr5@>KgX(*n zF(3z8yLUv2nQ2Hx7Hj+yD!-eppOit@{)0@yJA@HJv9pdmEJDXAUMchGOjaMc}` z!+UwD0L3droMw+!6l>VY%ya_~Ws6o_2-lBigKI`(kR<=LYwhBwdf6~eoUPWN5a2+= z=^mLtf_A0qOU3r*0WT0(AP^8|nj(l|lonDnHgOJDULi245TMRxhUpZ!)iFNx2NUpa zBzlFwc?y9-q|haG7BQ;e{v+x=b$!Bk4&VCja}%e1A^gQDQjVysZsueJ(a#Bl>zCBR z)2m^-r4JqSd+jAub+p&YIj8n@CP||GxtM zpZb4c-tzwsuWmg&O?UVf9s!SlN5CWC5%36{gTV7!Kk~}ezkBB<2?oR&j=WPw6Xh#&ea zzo*EBP@)EXID8ca0=!`0#Y2yB^?0y{fyA3hW|P=}nzW1R0}w}p?;QdTMlgW!_zMIB zee>hthhU)XwtW2EaKS*T=~)pB!2XV;wj05KY3qVF^s%v<2x?ziFo0`4S${Z-LaJ6& z1F%y!mIwy$6PBd+e(e+LpYjZnLaSktIE@Gfy4LzFS^ZS6*$}QtFp!`itDp!7Bf{`3X72^#1?C&muvyp%wg~_#ti)AGMw}?_WQD=k@ElxpVK% zoM7RlDSiq8Q`r+<8kP!Mb^8b-48N#Xx>({w{1t}_6T0n(B0?6ZUm;laaFdbPx742( z7>6GAD4YZ0qk4b$>GjZIC6`8Fz|Ei)`nb`%XFV|PQMX@kC`4q+VAx~|jh zmQvjC#%Yts+p=t=-g&k;gOn!F3?)~JuWgdRQ+Zc_kHGNf(C}qk1XSG+pMKZUN62_e z(VP|m3Du7T2yqU3az8eWDFB|LarFYK|Iou7qgK!5!?ZsuZXVsgG}TWLGvPl19M&mp zf{UuPW^$pCs>!GEl^lPBPa#E-27HCEPx(|iatOCNxbW(Z%hA0&({J>DF-gQ+XyBsk zlO>)2iLmzF?liHMlI72)g>BFPDcn7)v!NU{XtJKF*7_}3QdKV-&WXd(3K)MXBvEcE zHouuyYixS||0VV+I4W?^WH6c!n3xupp75U}{Bsem1+n>!SKBzn^Zn=X{gloWryoS6 zq&6J(@J>X*wm(bUD|A2icjI#3Z|g$$$NZQ^>(uKG?lNh0v=_=bhtGA={Zv^fX4IPL z_{y%ykbzGNR zeo^)uUnM2~zisP|#i7sKq3GuSN720`Zu$Q|xVrTR=kOW6k4L~G;1Tc$cmzBGvk-Xx z={raOnj0hm;Bv?XJ0i!4_zbUh%8b_lyqJi!1s($Xc90K zmi3t82kzd{equ_Ks(;||dqg#VuW;kM@>6nVImTJO2;3PN$o6}k7=Rxxgjy>Lx?y4f z$|;fn9pnin91%4F>==$$1I*9&PcSh-*%QF6re#)@Ht*s~1T5jOV#Xw^XdV#nt8f(P zYx(qvmy`$)YV|a+Drx|vb_k@@j$}A}#2}UU5UcrbbU#)P{ifN}CGo%SX)mjDc!L$G z0i?(o8RkJ(w;ptqBoZ+I4$i$!a$<26F(nFHeiEGG`_M<2wpEJ~0NL;mc0ad+hJSWakD^LRz zr~yz_LHALzhLPxO{XYMHLH_?VmR=1od5bJ%wGW6r@SD7PIY5j`D4(>{6b+p&YIUD;*1p)Ca zf2oe)M;$o;*)sdMfE^!@baNL*)onS5UJ!7@1p!@rtv{qM;{Pl0-0V*~hRZL+*h3F6 ziMSd5-?Y{5$kBZ0Gk4@2@&EC?C~W!vM_0EV&0;TnBaeVbz$4%h@CbMW&OqS#D}NpO z|E-%u|3}or45&~_!WSP&%`6o<^Ys4``k#a$j(q~(M352sAD}~Z%@Zo0@Zv~(acJwF z4~IT;&xfA=r@~hogIr2#U4QLs?~2Xu{hFNEbdxqwSZ}gjw8@KITJm2}Zo--p-oz5N z`)!fbd7f<0ualpj;uxH7x0w5rr@Aka?3~O@ysJ}eI;aGIu-GvW6*T~H2S(3{fhUPH z75|F^cz-n!TAxgW(L^`Z{CM;ox-B1mH(U)+N^y@KKVkOCGjx)wO_s2z0ZdyLyrGYc z-9!}o(rN%A_yw*L0q*e>Ya|Y1Z4wI-{v&d%wHgI3i{Wc*Ts2Grpojsw*7_~E9IIZl zAzYIffI1yy`bTj<-AjIRCNvB-Y5|{x#nAypG7*=h|^@Abcc`)A&j6`l&1 z1cRzuHo=qsOUQrZI>ND2SQL9;oE+S`q7fVfl~Ydd--gP^|JqF5e!2_!&x)Kt5{!ix zC9l2f;Vz<9PkR}i!x5~A{O4R`#%U0fFo-9DNSD+d2Krx26ix$d*!&oe6wyWX01JyR znxe6iURDPeU)^yzyqBl{u?WXtJ)*Wfnmo)FEKCtPb74gAd~%NxKA+MEn%08Hr?NBl z7{?=4wRZ7Ty=*uq&P^*||0(W;ii)~Q?D^)^!qIyHELlpK$$3SrB8$=5Rur7+hF?d~ zc=~^m{>RUR%Ss>{hJ0|Cygk7s3F+7+ot)8}tWZM@Dl&Wq-9UPQY*ZkTFKQfzm-d{^v|#id)3AR}VjrC}YGo3g2P zjGk!waq$0Tj9?5q08Z>wS77_FFGoOAa^63Qs!)MZjLJ$OYiNAdSyRVGL)deNeFy!X z9OSKz%kf<$|IZO8tmYd6AOs$Us3G!kk8|!k0%OnryG0TOM%8UOh%4s*(KvSG_C%Z# zv@4@X=UZ>f*biR*db|mUhqn3+LGUIU0(=DiA5SW!J`pZIOJY`54NpIZ+qU{01Kgp{ z+>tlJ|0hw)|KGp5b^i=9!*}rrcmzBG9s!SlM_?L(=imOL5dZJpAmTs5RvL{^1EL_|c%` zI>7bSalc$*1W-n#$Z1Dbq5gBM{znEMN`XW3a61KSk4Vh6&?<%oK)2mDohU67HXxqdIOVEPj+>(|nAe zaQ|Iv7edu*Him2B{*BHq3P}aVq>{>@UA(p1b*VUw#D`Ralkq~$;{$|=&= zIIskAXgP{Hn64>5R%hx<+(Ets_B44{`1*!P0Pt^Q*x+EFV_iX*+vNwOc=sHsRB!R$ z13Osz>)|L7oW7<0yx=(Wut({f@b)T-0Z@j60g7Qo&Tx@ULNTgd0I-C$iU{9RUaMjk zC-om1lTAc}8Bu!!{`zzxbt?d16W3n#EsdYyFFLZ0T0Pt$YV}+`O4lR+5DgXC>H(#C zk<#L@U?B``h&k5`dxe(IuPCvhBz}g+XPgNv{3xE$Wg3jp9*^D;+N zk!B&>mc5pW(;2=%b3hdV0NNL&sSqkZ5!LGP&Tj+&ZEF`#)ysx+;`Fov0f2GU3p#T> z`Gd_5dIi9ewUm`IFHlo(J7%gQLa`>g7XVBO0611wWk8)L?lQRV|? z76&myGX+)qf>cl{ZH|`L@c*c+rKAtbn>o)5jgR~czo3y@rtaV_l~zZ4v7B=N_56Q} zzvutOk**(&ip$sI&GY}Q6QaO0aQbq#IRIz4d|4M<+++@yse29@_e+Uj$poEC>Zb5Dz26L4cS0UbBk zhi%|yNgU{c4pVw=A@w61K2&VmXaES!j$BQQ2B2$xJlxO#wB44Ex0|j3(E6R0&o4&m z47M5ow@iS{)uE4#-9!}o(i(sg<60uuRAFty*!Eh71YmY807x;a>#XmjAEtyPh9;PS?c}b6A2E2>^0dR5quetzecp7VDhUL(a|}fjxn*;r}6~al9DL z<1LMk2|nj8jo^X*?>p!>WhvDim!o@m{y$E`5RZPj9ipeAvWm5uVp4irf>=dd9 z0FYD&{{1dxsC)`j(OIKrARSukw**kt%f@r!=(GX>0GG3vLP`}fQ_jB=a)Hde8VznX#5PT3pY8v=NU=eF&*5M(&}ihm2-CXwetVj!P;kH zyU_{#{eIID`Q%a@?>D-+3#00`97Lc0&z7w;1IrP7_;mk4Z|p%ux;o!gWnIfE71njM zCz8dED(agi+~JEE7w8nzUA4n?k+P}jHr&MxWj)f(K1$A**G`;miT{7g>Gz`#%^Lat zm*D@4mjD0Vt6RT&E~nx9c?3KH9s!SlN5CVX5qSOsKmN+qU);VyY(M23$tK1}9GP)d zkdt z?^?U#QT4LN;{*?|CjOsdqd1hNL7HNm3iD@ktHS@sIVoBvS4hp{_lPR~p0i3U=N0(I z5q*zzhK?*;1B+e+Ae9NEpN;UG=wW?f5dh)kuTIBg!?zgssCuKQ@M=$DFcS47!9-8Xm%QDbPk3HQa8vrV1m z`G(n<)bVB;?~1aC%6(MF+a^zUyQYbob7~|NF(b6B0#aK^%1>;v8U?_D5&&#>d7*YG zA@Zs16pt;VMl#TMaM9HrmxFtG1pwv-kgP!lu?4fm5x}}HLVIzOoZ}cSN&V2?`s5^B zzDSEklz*zV3#jU4<2kYOuRsAn0|F%rnTa+9x0#Ds3QMVTbqyxEmI zf`M7R&l*hX_GQ>aQ5`lFNjzcvk;S!M6mVxUaRZfqo>HURV@;{jm$*w{^ku`lfbfJp=~r80sIY(KDv;DR~;aZE*FwfYI2 zFUqtLl3GLnU27LY)oV6}YZ3tzrPSZ3g03pC6i-##u!sOKO`cJvQSzW`!7T-6@z3% z;ScgHt>Njp|0R?aL48t|lAvJCB75I4t%m!jXa#7u2v1T3$XOE7vQvH<{ahCYU;NVf zD)wY<QN#0qtkyIkZ8#PwuG*lGkW*o~icYK4pz{k6fT(~t zG=9$Eam=tnwRQnjy=*)uPE9Ky|BZAikSMD+*H`4?ZS+*VY|+L!Y@L?}-1; z?)nsUa`?_30gr%3z$4%h@CdvBf#;w33Ecm$Z;%uKv3!_tBP3sroI^4* zyHjCz|!&#CYQ_Wxa5_pmthnR{4V zxfYGbe6% zY4)ECYO<}wr2?LyU}dw$si*(v)Bh=+-<)so{)XqnCc+j{c=~?{X@vqT<&+ncMOGFi ziao&SMz=P)GbSINP7TfhORa~lJ@%dRX-0kUn;Q#v`dL1wdK2Z+zJ?({b)?{A~ z|Bnf5T;Q&NJQ8e3!esTbHYl>#%ifoH>O?6505&9J#LzFO!sEvw`iikrlJ4N*t2-`- z_woV&oPNkTjsjGF;w<4X1elxGu zGU){X=LrCCDMTq11+m=t@bR(FN}Toa{WyJ=H~fDYTG}&1;};nc=ojP;7hMN;rL;QQ zYvr7+eVzP&%*|%TCf*VHm}w0AO-tmXrZ_$#>EycSzisP+H}tWwn+RdA zfc_`W5B*PtWht<1Cju^D|0&N06*k}}JU*JUv2qlKpl(~c2&!JQAzTyt4--N*Y8XJE z@EflV9{_iq<;L;zg#JnM`2CO+_P^)vIn0!DpgG=Jq$hq0SP$6mbqWB=#9~gsdp5pv zB8Qh&07$A-9tIFoQnI2iyV~g)y#Qdo0D$_y#Iwo&2Q;z;V|&tj0l*T{D$cRjAw4~p z#MOw@ReKAkuu2i5_-Q{ov4;!)Z&In9{C`wgWmS&Vr?+~z3n|Khq30P>eLeg?`bSun zpuj~Tjj%_sNx>ZX3b4;OBbmYhR1MLays-EwYOFEtK=S`taM}(oyt?CZbT3c;7bvKr zA_(QxyjY_a9SY?ZW_eCQ|8s3df9kRoSjxc|Krjf zg#o%p+`)^ol8j2FO@aPLa#GG1oZ=pma1$6{%;FMLuo3N~cq?>mT@Vj_YzXJ6JZgDTYZCreX(T#@bBD2R0Jugd4#|* zDz^0*u5re94?&8t-;-B+rhEE-lKzjdbE~kS5iwLw7{g(zJ^z0u|4&v12c$r3^|;|A z4*yS?69gACHg|jkSQlj}=+{YTe6s)H4qxCMe&}#C%XP=)GkvA}KgZA+3uQ;><4idK zX^8w1z4h^iqno=hs&30c^!)$J%m1g9KJEyY9~03y9(*F9Y}>j+Z|F03VblYB(7!0D!oMD z#YC+wrVvR$7WcuPiJ&eH04Y%+M-o7p@^MK(*Va8Qij1J`GxxZ-a!G)90GK<{L;^ro z;c5B+Yg{5`+REi7EIg5(tKmrUWosAX}i{dMEY{&Exko8U;Yd-*Z;E zC+8KZ7fzbNNbHOLR2IP;+nN5o4xsO6gFJ^TxU>!+!2_Xyy@ntuO)+u{VJ`$&A_PDp z<|suy{lA3%CyyGU3W7c;h~=ZIvjso+L`j15j8Y%M=JT^4C!4w4&v(P(=TxH-S%F?C zWe>M{=r_$KF8qJr^NcCJ9{xYglRQo$EEdu*D_8-o*$)4rP@h;%4V_Q7hc_QIl3$0{|jpGu;a&|2b(OX9<@n9;76d{FAP(tGPbn1#tF z0U7<<2&6-67f{v9#&hD}v3?)4F>a6YYF=#gJpF$j{f~;51pg|4Q@`P~M1C<=!>8r{+vYBes@rl9 zJ^%mm^8b+fO719JJ|#d#PCGJ(+qUk|8~V&0c}M(zbua4j|Nrpn)*mipE_`#3fJeY1 z;1Tc$cmx(A@Zr~g8ruJNZV>HXLim%Uz%jTWL8-JQ%%1h1%f$-oUnPB3!bDJvn_h;` zsLlEpq=$_Jafi0ofO|xN_Ft8v(p{q>-7enROZ!Qe9*B7x6H z225N1j(|7xv9X(oW?x(ez)~a`mH`$h)QC?c8BK8lOLPDQgtTG=VDyU!5@+O&EINR$ zwF{x@H5Sq{Ea%WxAwB>S>EpXsaFQlR5=&8@R`zHfA0Ujfrz$wK{7pHy96mvC zk%V8VOsIjBQOXwpehOVX__soUp`VTNoKWJ0g#eOgO8$BvJ}1Z=9N!+V16ZO1NJ%RV zsR066O)9JFVvi^YJ7!_|WC%|jeWZjv&#-u6Gmb;{=+@J<)Wg;6JAIEc#^bngx@Do@3-l|g5cwo=vk4u;DGGo?2NzD=aXFeRPyitLD=KU& zaR29gBf32`N^!19seLpd3V=xk08=in0u;;K6O9XlH4wc3;5-39 zQ6(&7XwO(CMTTad;*tC%)KZ$Um7~!`6(vX1e&-PyKMM;C!5o2ZtL+;)xC^D#(OxR& z9NIq#{C^~&v>r$y^3ghu)tl+~f79HBQFU7mqUZl#Uj84qC}w;=`h?48Zjao%h5v8c zxtD+6B4*V-XSlmS##o>Al#ijCY)i8c1eM`8I>{Q8cgk3(Dc z_&D^LdwlfDfSANOgZk2{*VkYB+PlI6+^?yVoo>=53hPa_i#B<&BlWMu)Nd2k7@j88 ze!JfmNuB4(22)HpzZ60t-)_kcB#N-dzWO5vyP;+zqmbcpl87M}5feyVm=z%aqz8Ti z$AgXF#-aJ~a65EcKHkYNzjg^B-IMrITRVaWQHl6_&FTRic zrHOxW8b^{CKoCm(3a7XV96-jqrxjJg>r=5l%f;FUK+NWD_{$2^NWGRiv@a^p{%)M%kcrTZ|hEA-N&B6vksSRIL+g`j~-ut^@F?n`w#C%{q?(u zc6r>Ot8J2EyW8cvV$brDZuzDws+jH4unx;j*;G69iqW>hVvKutof75>!}Ium*~q!F zkUc}?Er>q*@Lr@T?%#8Ebk5?a)O6W;L*t7kU`(?s++p9rMc3-M9Nv}k|1rb2i>giN zOM60J8_pOaA2H|H0F{eu3Ej$%$c0gLTMnY<|6gAIza*1uSRW0SkIvT~V7-Q?ZR-xb zq0iirH^Tq-{{L@Z-Fo|DqA>iI@d$VXJOUm8kAO$ufWY(bxC^uY{taUG#oIrkpuGv{ z64L5!DBVrk6Z;o**FNc?iFPNO0IOFXVLe*paq*`J)wLbQDPy^ixI9!3&VyEH6SLOtHR-v?srroxH4*)&zXsgqr{l_bbb zN8g}5Ug zZLuq!^IyREXOiTfLlr;?Faka2{}Gb?C3Bk7b`0-F@tsi2`xWUYstW9wXDCC3^T#hB zK`=Eene5B{Kw(Li$shoUs1@Fx-8DG^>)z!x}=t*O2~w2mPijrMlyCcrQ=-SEy6UGKjG$ z!@E8zT>pO#p)3)n_j>aA#jjn|YyHl|@l}#BM-Q7rYZp+}%f@r!ytD$&AN5%l2L?iH zJwoTu?m7QOJ3KQSl_bK6G&ip{*XsHI6Z}8xSQN++ojYB1p!&O*kI(-5UJpRV#aNE`$dPASNBkzd+k2!+2`Ty@;-FjCd0sfaqz$4%h@CbMWJOUp# z0?$7apaHnPK^lMz$ujprNEgzQ@FcZEJYECvVhwv=AI2b=YL~4|Fn|K;3`PVL@a&97(&Iiqm2KcWKV_{gv5uZ=ZomEM=l#= z-^j&8H$NVJhi=Qq-x@i8ifF{A5fmOj$68^)Px$+Ev^L|=)&+0qV`DcF!@fA@PmO5# zh3usLWe2zjZp|`FfW?PWAnG^(2zw8LQ8u3J-?w%lRJ~?nxF*g&CtC_efOK1CB*c{+ z!(PVu_dGs8bgus-y?6XQ=bN9GGtPUDA2$!5z9~&)UlFMQoJ8!_3qR2R1%sS_la1}1 zu;GPM{)rC~uoJVQ1pgeg6gIG)|6joWGeXF=3J7PL4{So!r~EJBte{y;E;?TS3_&9H z4w!_|AqC^^L^;V2!2gRHDABo>Bv_&^z=Va-u2;&IB2Yc_n=;ph|3CCRW3sO&=O1by z74=lmYN$+c33W!wVIwfSJ`He7=aaCRDu4}*&(CK}+Glle@zoud!+UxD9}=f5QdtBf zZ5X3}d)$)rUy8=6Ggg@xw;}+L%ZwucAO}7j(^L$tT|`9y(Dkwboj5$LKmd?2EXle? zEM9TZtIE(m!g~e4lGQZhB5+hb!7##I#sK{gbCCokwmqQ?1ERJp8fotqpqzW6GP5!bqdyHXndzwc*W7aG-I-f$dH}bZhAsrXEzukpX=60m`yi#VN~6ggScM)Uu+ak4@ef*3N^J*5eE80S%`E3DF8 zwPSG$sadxTca-)bdbZg|$w$_oL?UTH`Eqjn$-G<%|DQ!I|Nq-pw|@KMW;^`X^9XnZ zJOUm8kHE`_!1EvcnOCm<;=6AU+n+}$i32haO5B7csl&ScdA8rP{mC|OcIB?5kZ--u z8mv=CXRK+DH?*0vh@K4bn(Q3!L|c71jR^YpOZxtR_G*#%Sg-T>+6OD zytxaQR{HZ2>%Ot!!8fFf`FNF@1XV<}@3<^N$}BT5 z3z0i%j)mDNI;hjEsmb=QkcKAA+_~;ZfG*4SFA)ofF>4R7A?-2Lk}04eG^Zr72k1NKHw9naaXGw~XZtC_gWMQT zv-Sgr8Wgg>f`;m@j1%mOA^Q=eDr+gB?MpU0X9qHn3fbSbb^%qrY&<89N-H4yF-s|> z`b0r;EDi_u*z{EYl9e=v-_LOMAbpyW6GkkeR#H#(Pg4Dyjw%FE+Hs>1-dXMJhOsZH^Cbd*y%x4R`_MtZCmWB|(OxO% zoZ{C(^-~8CQaZue14jX@`0Yfb1lywm5yi>LI9lho7&8<~KC$Q?~`aOYFbz2Uq zr~U=qK`yWy!3$IWA{Xr?5`FA#5&w@7#&D=Kx%_x#j8E*!v~|bd&}S}qBl-VX-tzxH zb#?2fUPg|_f6pEPkAO$OBj6D@g~0P)4&n2k+#o)mYuyB~<@x*<^ZA&kOMT#q^3^1t zCQcRB0%j#G`ylv4tV@zwFdgN>)?YSEW3fP4+Y~T$Ov}9B|F?If_2Ei85dtk*B6g5xwG^T z8ooa>(QQ5COxRU5*Y6pkK=}T)+w$?a0>0ldRCf@C?w^;f-ctCbN(402eLkjsAKJQL z4t;DaCt}!_ru$Lo5NyItnftqZU{B~JY=08Qf>u8sTi%|`t>eW}*#54yi=gT?8^SfQ z{Z!+|T_TW|_Oa+dVf%^C&PWe3Y=6h&_lRo#UgIWHoc5e}eo0O}gEIi~pK6RblrPSP z$987_Y-BxkejEu10alN@zCU`eqqG9)8Z1h=5{8i6Iu7U67$ zB@Q64`*2)`%ISTmkqEdbgE|?;#JCiZK*AP}FVL{i8j*mMz!Zeo%!h57aO#94M1bI!sl9e+{Ii;3KwDF@MCzni z;CW5Jd71$6&>=ShuOw` zmj92Z*KpV|Tt4NJjt8Id3NUT;u$24a&}Z(*JL3P7dr{Kz|9^6I>rYPcAO3xhfJeY1 z;1Tc$cmzHk1U`H}q9VXA-ylr^rjM`@fxL8&b(!&^f)|Sl?Bk-81mKQ+I)p320B(8e zxF~`EvBf?cVAWfAkh2i_jxjBZfrM*my}uN}iHI-~V1p1u36440d#|K=fRm~wa~+8fYpa1L0>`fIz7v1qk(z0tDcAgfcpdhpepBMf)N!W-`aEhA*K(K@EbZG z$zH0Sk~rq1`Gaea2iuH7YEhb$!+29@vjx1Y#y^C z7eiqvRSSz|w`#b4wMb@LR07kq?a$&!_oMAG5&;*xl_d6~>HaLEA~MJJvZ$qjhffA> zqWhut6%ioBJrWjaEvy%rJdF+AKaSQP$@){Xf-&3^-Ot_u$P9N@>H?*9$Sq43zQ3a& z|Ms5NJigu2re5Fh_e3ZExkM-$K0(&ZqIf;yAl9E*_8&!tG23*p4aplmn*FCJ5EqpM zvX8b3t>vaB&N{OHTiAcrW|JjQ;rzhYNyixJ|2sG=o&`45@c(v(zt>0Qz%=uY%|2@@9?k!^5HQkHr8PgaRw*?B z3~$^4@$KVuo>1{{F7(r7cZY>R(;lF zr#c@H91TTtJe}%ObM83Bj>wcdG;*gh?bG(q4&90KEjyy1&x-Ecb3D;0{67A_E^FR% ztjey;I+A8?tkiH!7WUUxFLxg2S%><023BZ%QCcrr&j@#DC-~?(6Su?ro#6kazo}iJ zBazQ@Yhi)XhatnoMuz(^s%hInjQl@uP4;+~i%o_3UThSTKe!2io2UF3>;Ln7tpD$x zJndYrBK})MAR-VEhzLXkA_5BteEk;++5!A`9}$_KnWq8JKtrYIkKUiV=kS+04k|KynGY=3dc%UKct z_AOi$hw7M~dG?URKbD#1Gk(0CK<-@K;iZVYjy_-0%DvYO}pr{QEmm zN$_qh7L;NtoYWK3VnsTvxJXC)|8PM7-zT}yCC5!Wfu?d}r0D*?g~Q@*$6x^2EvG;& zZavQa&oVq!W=oY-#sE72=^G?hkUaoKQlDNBOZSqI1K1h*7?7>*%)Dc-??(>cCT_X- z@skfBs!cV2r2pR@KF98bh6hAUX#UVt&_tz?Vtw_X6#l<0Sg7N#t}Ihi(a*KScf~GJ z!{pN|Y=MqZqx}V{X1U9AC3xBc|IZcQR0$X&wwDXZeG*0ff6Lif(~1J4BxDqDbEy5C zH5uvub^0F#3Z7kvq3B#Mk~5d6j^+RN(f>KN|Hvd0qmvZ1qzfzUCDYC>)Rkk>(D{V@ zT6<<_eBMWxU0jFldV+scIuqT)G9+$)_vXvt^y-T*4lge)r^xH{=lWdWym3YrR%AVa z3O&v9p)b!pZ|uB34n4sL+V%qmK=_BI&AB+A26Pv7eoxCO_Zr{q{~!7PN&o2L$V;B5 zwQX*{vLI>6+40?+Z*9fp>-8p2$jey$fdrLm&h}*(`F}A53&m~0|39xr{{MHLJpG*o zjwAk3L?9v%5r_yx1R?@Ibp+o3q{I*4zxtRY1X!50MFLBUrdc|QyQ#@2BKWX~zzwpa zXkbfH$Hgvc1lBbIs7xfKkW`eb;lXUFYgu(Dv+ghlTf73F-hkH7fiT)sg%6En@6smT7O+QJ#zF7UMvgLsWZdh z>ds8}lnh}6thDs-`o*J*{R`~Mq};OOg+f!5&V8fQu&t=orYn6dy)A6NINJ!fBFCkq zt4*;#8W+)BY6MLLq$lVP{fXPro$e_sl`8w8In~D#ofNtgog=D4b3A5;6a5l~YJgd( zo2;XZmAcboeXi-VaLU?a*BuYXz8;EuIw)kc-C^k>mEw2Dquwi0hwZ2J4qX(+#k>Y9 z_4^nHh^37MRd{orhPN$u|S$oH@F{X)Fb3#p)^6hDFccdw#Y?q8>Os7k?-{8)`GO4{OH zmbs4~GC%x%`Ya z-2YRi`?C^}VuivPa}#4Tzg}*aY1`LZWcTq^FdhLeb}LEjN7wt)>6icJ zmNudC1FvEyYe;&Vva8$qEh*q{3h0@6{TZ4b*WiWqr$+nBPt6L3a8KkuiR+qbU%Zqv zj1=Jb9XMh4F-)Pq$4z3zu=^8_Z+Eoj?=^06f3y`r*?%p8nHkl=v@9eb_+80$q$G#k zLqYBtJ(g!UlMh9Cs{4F6kb*1HL*E^b zebx;8TLwknpsk~2H~Tglnhy7*EI(vz-5*bfwi&Y1a0ht+HPEEC_{vK5GHj#LI=y|U z8iv2c#in7bJp>H1Ur3QVC$$D_WDW2Luru_B22E)lU}t_KL?5jQAbzRxJZajjX+%GI zWSOoeY_~3qI44vIZo&MsZ;^928(bSAn-Wk~EE1p!eI zFxT|6evCL$)$;50X6|h36`((m5L3oKEl97loO+MWt@?`We`No^jRpZOpB-!c*DI~*7EJLA zUcq#|#DLyVc&gHRn;f7b^GFKZKIcr;J`S&Bb@i)l?8`C|yLU!E6n0tnk~U4j@ye7y-lc)5Lj! z5ish2akRf0)hu^4t}p_7QU}zomxq_m7IlF5c}$CRg<{b>zTM!&IRA~rIaB&Z===`6 z>wFU0=AG?84L{m0CcA4Qp`DI*QrC4n%4%VQ|@Z{)sMWg0dgK9-|$j+2DN=hCA)*!8*T zOz`2<6St%Jc>2F=Qq~ad!to0x9<=)ANwrd9oY1sXtKM9tV(BmH6y#_2Y7!!!IL!B? zvGo5m+6Pq4a`9Zb>~}!_6G)fP|MZ|}cxGc?;q3NE|8F@t5#dx_f?SFVofnGOOsJDH z(*HMNcWTRvM2gOY;$1Cl21P}jTr)1&8 zCdD3;LgQm1#FAg;|1T5#lhT>!o|Pd%`#$;qBBgoX?Vza15Nx(2OH{7IRQ4D2vAMp& z^>80XHElbHZ%>CeKOEk@d-lc9zmWb1FZ$1(f7Y~^E;eUmrO4xprmv4pdB#KUa7c$N zJ<%y)IGhF?+48L|LZsUCRoC93IrV}eUU?jxw-@Ba)8DQ}#pP?DBL7cw zq-m`Q7G@(8XV2SMYWb6?YGJl*m zb+kx%KfIb`bahN9k>xyAe=?G?c1~!txN}PYGLAtnl%`Sjgn1#>#TuLGZDL00Ul+{A z(f8o>+rRxosr@tWIa*lrZtD*f@3gu(l`a2k+C$nml-70UDf~P9g)HD`#HYsF4BOQdtUtD1iD4Ju9QY%Li&A#zO6hMj1 zQjNAA?qjK@Z3lD*q5$`aEGmGYB6lPiPzANZuc}&jMyc@95Sh(;8LK~$g*eY~v0KS% zKiVUJlZcdlT5wY!2Enx6Ww63K08nR(x4p{L2%!&Klk-a&Cqa;0m>sP@GJAQR6^!AY zWB|0wl(M$0W-;Nv;I&s58GzLAg2@d+Q1kdN2}%B22}`b#lwsU4&=T%v0woJNFH??& zF%S81^#s6jnNnO#9O?HhY6jVw4nI)TU0#YQ$_*gt;%aUndoF?p1eUZ_(M!+l>uIF^ zb-{6-;jYM#{Ojxf{OsMUXJ1^puO9lMJ7)Efll_<@zA8@e$J_!3VuyjH)29f;%D(T* z<5BFW(|uLsAQXpFwu(R$Q*h5`Jre#~Xe&~^V*Sx2IV~&L$rEWh z8vu~l&I9Vxg}$$GCqNOU?rh5VDI4SzUbuFg8U7j-sla9C_GKzdSqP^cGzAz7)a#@Y zLuND)F2D!?$g~FFD|~)~PYZWBC=rZv!J|!O1K+&gc_B%YDOQmwZRIr8YfX0c zG(!Kf?CJ5`-53?UdOf^+`+6Aqcf;Fv_T_kP+XL^nbALY3$D`}dEm1E9G&KJxbJ~9N zgx1-yJ?4k=xv9@|D&fVL6^CP0or_~x+`$B*LBZkLrgn%>9PlFle;@x3SH_uTp2CdsK$RpZnzhd6MS=BEE^x zM>y&aBb|g)$iyid-NYocCX3_rDc(ziD;li?I7J&mgZOK0_p+1Q^Ha zPvjXg&k2nMch2rtL3Y8|+*aTuwtfBWfZQj0-+(8KnfVFDmky-c*nVj58Bk&F>!R{lRI{0d z?Kfk6H0L=km@6slhqL{(^DR0oY$;wdKLG-@CRtIV0{#Z43V%;M1KHadKwXho?x*cSE;Xn(5Y;@s-Q+WWPdM{eB&5j} zl84OX8!GEQxy+PuKJ2>^AwP;aH+!QOD#3l?DH{dczo!i zOoRpcw3MsJnV~;4p`|DQ?94kh`+gJv+$*{GQ&ezVsHohh4FUjo80l_%<3tq!0H-oi z3Uy1#Vc4K6lD-WZKtb1;pg$CJ^~CMqK3)KT5Dg&}8lZ9Qy4RG*P$}ULptkZ3Nm7ac zV_vcRYsq0O9E}FeAUcoM9|@jnmJ8=f@U#Q|pRk7ul`DyDOCxsaiKGtF$p3FSND<;x zP_3My1Su$OT`Z#kz^VZNaV3hr7MFSfip$+uh>`!lkN@WoZ0YfZl_t+5iv8WIr-8`} zIV6P&EE`^j>cK8FKIbIo?BcyXPw)>)XQF#lhQ#oD<^Ks)raNdQLYnx0Dz4}2#t`{D zwH7E?J=}*;P1_D)*%0P9@TX6nLMl-u0J!h!8~pIg4}^5U;@ilvQa8b*8eu&D)s(UyeZ2GVe`rGEaVmDu|9b7 z92dKlB=*A<095Z`3;`368kWWOvz!3jpa7t@vebqXVo1+Hx@`L|m6&F<522bBjNzUX z00o2;<>@%t%E8FF=-w`rh0#^L3)s_|$G3ah^7lk7e^nwE4W4Xl@y+GCNcP;_^s=#~ zxQ;qyYBtOnv9ZqW!Q4OTQ^-}!{t=|KIBZUI-?T`D`;W!{pZ)5Km(Ox)2Um##O3dl+ z(I$YH66ko0Riys6Fjj0f!k$FFjQ?Cw{{)(uC9GFMNr?QC4j!2Es0&D_|FR;vxMWZ7 z%+SYxxx@8L_Y@7`2xQDoivOWkAysKj!I$wLCqUw??dVuEodzoa3zv^Ic7dVI93jEs z%Sqk%r==zTFB5!x^~CM)KA!xS#uRA&D2$}_K?B?GfXcsPVN4tL+hHdWC$1M#!fJ0Q zczl|%pn`KRpo#srqkTZtEEms}z-bTcKR0TfC0&VRhYsJhRiRARQl$Sk9j0K(!b^Dz z5gdxAinA&s|G#>zH|U^KahVc-GtNrah$8a;_woM~{wRp|lCDMLM6$?@<#GKZqQ3pM z6e&6;^v#8Bfiy&ZTS*~~ zhDt6RJzRe#A(EQ59mL503%Z|X{XYf(Y8wlek1q`YkX~T2_+$@{WA!Jp^yWFCEANW` z@17Tv|Nmcq^7OAy97g;hA`lUX2t))T0uh04guvJT)^p?lzwi;s0Vpe`^>x)kK2%f^ zOG-u+B1AdBhvfh+VF>$AFZKX>hRmdtyt?c^bwrX((-EJI}`7F4UyT)R2zxJYN)&F+1kD!_r z4B?)*e~v?X>{Ll!vlb{9@+@rGkr;850csS@Z@?;Uslt_-Cmz^Ny{)9}NJ# z>Ki=zu~$F@M3aPxaJdN7YH0})Kvs(cfV;a8V$mL|hRLV1K)J+M>j^%*dg69;A1?u@ zX#xkah5ko`k8~+^P5{<4rKzpuyVisiA_3sr;zsWiZn>M^ht5Ao~A0X1qiv>;r~VE&vbk77#iQa+Px^F=}JcQJ=?I1?}U(k*GzvlAw0<(MgTjc-CbgcjX{U=X< z{~J*qadJc;A`lUX2t))T0-F$c|Moxn-jhH1%O8 z9@?XH`##pgsXeun?)K+ufK)!W>5yf8I*@~|QmHTe!K=6Lw&ZoD-0YOFjjDikRe+0@ zr~t%A{U&{2Q~(Vno#Z3twFaF}OOT~Ztf&B{;n&OUGHv^M`^Z%QxR1{G8Ajjm+8UyZ z3c!rjpIlS`mJ3%!)+=f3N2>rROsP0{F3CeThrLM!KxYKS1Qg zvz?T()Kd%_f>`X8M>tZ9b+&;*cT2taYInfZ;debx_Oys`jS^ebx8 z6aBQ$CGlfMkz{c^r03=cyWE#`eLx9S^!eeKpPF{a+H=JEV2=!p9N ztj4(J_SzIKpa0rUo3|PcA05$B38pa&uoHYZ^~CLHKA!wX@>e2CX{2(UANcAVo|vt` z#uz>6jb+Lmm*Ib3ab$plv7Hhg9~I=137Q)1BdTV(fUey6J7E7CivJo8PF_ZscNiR$ zeloKETV9EBdQ`Mhpv4cpsOmP0_5X+POT4P-wT0&tHUJfU zDgTd&jJyq(ad1Q+A`lUX2t))T0)Ys8{d0c-rvKGP#Pplq z!wDG)GUIsEd+G7W^nWW%KNh8`w#YF3BtowrCgKM$j`d^1c}{3-xGO&ZH#-}d{&l9G z!a(5&5()#zd8N=;PI`gLgA8l-5aI=%vFfPgo}b0W007&<1?m;0z-9RL`XdK7PTRf! zcR>A_zQ{lBlH8&N5QBfEr4UwM{45q0q-X)A(LRJ~RxpNp(gNUuP}78?OduJX zrbBh*Y;h3-a0&$2%}7zdn`_H9s0X`(Toza@Qu zBtN`xUQ8o>9OoI^L44hxpS^qa?2FzqvxmOuj#+)g-s33!DT@;gy;+F|^dtidsZaT4 zmwn%t$0N4^wZ7$0=f}1kFa&R^!yU{l?hGB_ac8x%CsN#e!0g6h3N3{Ep=yZU)aD0V zWJzVq{ihL>aKV|NKa_|8Bl*k3?dU#USb*FkEy`Lt3y3Pq0~K{jo0W}S%PT|=;#6s* zCbhM|U~~m6U=D>e17?kyK&nRTj|5OP%f)jg2K(9S3WguQcsIzoeR}=n_a}+;bdSiN zOeK*DHUu8D#*l3QMNf`mS1tQ~1) zc4I>}I9aK%a{;tz#7_mdSFeYcZ(k2X|898u&Z2j2+XIo^xj&!k_Sp4j+_h??XZ^sL zbE?Z8SzLB(kNM$zMqi(C#)D^ZI9AoUIF`j7=s`rMqTp>>RBiN~^5S;lYdmCZaI>=U z8c?pB90tu2Q3WoxSD(iEKvKkTnd5G{kfk_{dHv?qkAL#)Oh=>4!b-V9oey1J3`KK1 zo$6C_CWtMM@Znw6wWl)e)ArB~-HAn%9Xm`3!L{enhrix^{C_Rhd?f_ZkuqM;qFl!M z2&y^T5q$aX{1dyckVFoWLSHJg z8f6D!0QZ#_$$uWos}`b=)$CeUAe}tOw^lBJ2lvk@d7BXtHlOT@I4ZMhOzz&W__$rai=A4w)#&2pFDO74D7+<#8O9QKV`VDp1_918W%aT!1{$=-#!H8vg-q698~ zLxLA7{~=m#CLj%e&u>t)Ds^FadqM`XEZ5%_XNLccab~*TnxG8to5PQ6?x!E{fM|xp z(DhK(r|L+GzdT5KJnN6uK)r5tf{3GU!Vy+FJ)N=&BBDF?Lv^T+mE?xE5)FBia**Z; z-7dKBX+`1OQjzfwuQ@DeSIu0#!y@&D`rjDLxmqf5}6 z(LDhUJa?B)B>Y!nb(~L%3g?L$?Ggum>*`7)-+v$9UlMTg1R|FFX|Nrwxqz0%^OA6O1vJI z5%O!E6B;DW)T_J;C3#>;mT39^?8O(aU%h#Ec=_)8{ct{f`NMag{Vk2xZ~yiW#qIY+ z|Jn1;pr3{;?GM!{JJijIYE9}H+e6w@UQ0c}@qBEG9WK$OGTIz#yN*cW92`N^|vfj+03iXehD|De=d(eksM0&mW^ziHDcbT?*{q2$Z zG~sN!wNLFeKn$e-F!*QV~jUgAx%%UB=0d5(+SN(TDjb^k&?k*81ip+;D5nWjl0 zzoqV<`d?|$YylJ&-xPFRFF@h`r_nxyYF03Yd*c430y?!Bt_eh8{A&)4+Z$OJf0+GL zXBx!hzbR6BfdC}&%~4{U9Fazo$lB&N_Hw{=1-V42%x&&P$bPg4)CqG+*zcQ={8TrU zlw@3g=bRb-_s*H=e)mE$mX9_Bs4|Qz>V$h4?;g3Gog8q~2W-#>pb4NLIDr?%mYDX~ zuGAt51hxnSU=!G%Rgut=n3_b`R%czJ5a4tPDgJ`*rzrz25Arm)>~?s5JOcRtkmKdc zH*W?)CHYXIRA+|%&;+V_<{i6zKMDb&CZ+wwAw>#EZy(me@8j}9^Lll93a`)M4BvAN zr!&EaQ%~HE=Hmqcc!1Qz!bqy=G)L{Y8_JdqfpeCMHtY+Z8|Lpsw zA8^K^)UXzFKG?7WbSz1K@^WcDH=wH3t7}81!oa(J(&$F1mt2Iqu&)DMirxqFy-Wl_CI=&vL(JU&G z#<=iir}E6(>~JWLL*JE0a;2wxkaGie9^OGh7s!q&Pk=aaxzLK63M% z`&Q!4v=~x%6F@@3GtkL(MhCjqO)ADY2w6}# ztyo>vid!KNkaiQK?K#MfsSp^xR~BHMPJjduZNdu@$j0LIE{^HMxRNnkESi-t)6RHOArf}ole4B?)%1TZEn#RS)OaT-K2 z@ynkPnfwijPE<0Im;iR9v*qk7>r%-38kg8w6mE8D3Tcn_g%f<~Xw)-&c%7Lpz#$3A zN7MbN{ZLjUluT3J)!I8{x{?jsA7ndBL+wx8{~rw-M_WZLFbWE3o9pcQNcL|b`#JjP z2vkXREX~v*>|HFmpRoO~4%sVPU26M{Mo8R*E?XVOp5vJk_=VWfnQ^t%^~^iA`hK|n z7BBfSLpf#b033ih35|XK_=_v}3YAax7)6G9?FfZmNrQ!jlhm2u!>cE5NB8mEKW|l> z6&h5mRKFsJ5b=Y$p&%ygRKo~yYNL@HrY6Sa zgj3DgdC?BwK05%@$)L*V4u#1A83)N}sRIqe6A7Ac-V##YpJU;rX=|bJX~AvTR)1nLI^zFt=7g@iEB?Qvjnv5h|Gg(q|DI$5 z;$IPgh(JUjA`lUX2z(?6ywARRiX7lC?oti_i6cgt^P_T#&!-o;=mDhJ+|8g}y4&Ha zCILF}jwz}YYQLl;{yJ(>gZ;2+o)j83?OzYTtB~#kHSMS1Af)xS^S+FNfDdOp+z37j z0=BRVE_Pz!8U+Ds*)4kgabkeOPdxX&^!zOjE7C)GL?j#tkYXH?~A3-%M7{WdA z{|KEZ_-(5MQv|wJOP-AQ#<>3JY@`SPpkQs{d=KUKBmgaTU0U#Ln0zdb%cZMcGy`^mkFTD%9o`*C0Jx*jpwVVQe&DO8on*<;he--y zZv;<<)Nd_6V_uCy=0llP{FWNFiv(aCtv|97fN7Qs=t=+;B><0*02CB6n0GY{0H6^vkDn&6*j&P4Y_3yHRO%KxJ~ZbN`H{Q$1{e(=4_d7J0UVKn@|8SC$0s>-H0p(}5J|1T!~|Bs(M{o{{>?TG)G z5rK$6L?9v%5r_y(2)s|e`j?OZbi0%Q;3tkXc~vI34)gBtGrA}MN>fC5BbOtjJ{HB= zH%Z8Rl0$_2Iy#b6jvA~#l2pz-DKv1}zXAX&R^SEa>2D|J5d{EI08kw3;nbemw5$4a zHB?P~Zqp&l`gG_jDy{N6oIa+P_N3&A0stUfHP`g;+9UudrKFhi%4B_n%%|h{LI&Fi z0L)nb)SKtHr`}5b`r!hABE?`jrEW(0EK`!?oN7`UiIdFDw3$%)Vk4mN6Dprn&r+LZ zHQGl|%?gHaPXd4nnu=S30=cn1uJ~nwhaX4)5t6hp+&v8(GO1Fq3vuM^$rVel3yqgt z!Ojf*p}B;OQNT3Qg)=1o_-MAjrEN!3B~+*4ZX(5o&eDu@{|36hHV`H>z(gmSSDtl3 zI0u?>uYgl`J-=#J|&IBJ`J#jm_kEi<) zsnFD>rQ2KqlLhzafJ)me;#02ho2ypXe%O7j^%J(g;pS!|Xw)p*KaJKOS+;+g<>I*# zEk(Bf5o|vK(X318SH^WLx3Rl)qTB!KgM(T{gZ`h z8yp(xrIX=5Vh^qfRZ>>?^@p(3u=k>Y;2&s0_g^NuM_Nd@ z{avN|yM@eQ0NpRswu2by{*RpQF9=eIt!cPHKU{P*Hu+$WOM>o?jsGw6vHt(>JbC(e zCMqHR5D|z7L1#=k$=(Sc3IIn{?s*eH|fuG-;iW_b%sEvKgWqy69rU!b?Ax{=G}ca zoQ9#wa$L+&CRHbN+1WQ2M{FYdYh6)ohuExRI`F1+!NW4(?XH$wrn~E$xzT=<}rEMOk%a*C7VDT-q6? zGC=2&kFv_rSlHzuyn%!{~!(r(9 zL(eWhmh?e7lx23z`eQZF2Ayr*QsSH*8+!JfPNc%mb$9HC>QEo=+Wv*~I_NO5Cb!<8 zb#RGT1Ogq3$bh0R8Uh%rYm2As5z4(57PB4c<2cXQ4&v+n{OsMUXJ7OdN#N^_S$*Vt zdW5U5ixa6Z`Vk$lwjF3K(G!w1W#9MZ@dz0zO*)1;Kep{4t>UYDdiKynrNLvKen;p; zTas=r#Ula%YOn)rUvqJVu82o1mNXXPP~o@Jkjc8djMN{PHECyThf>z8kTHkXFD#h( zP?RS+K@A6@xFS9D-SOCG&A`8981v(bCH4032ieec=j@Q>2MYT8%OdOF8`|N^TRPeHSLhK=d90Z<5ixB0rH``1L5h8 zvga%oHMF0(S!L;^YNH7l4Jc)0yDIsV8nna|eQetjY^%B4>W!tFuZG z1k}h|0$!12h1#4x@B~j5)2>1uNt<)d0gajw1k9uLM@A4Z&vNlxiKwC=;1PlVIXPQ6 zZ49H}U~?p8&f3XS;vQr-w+}d0jf!KKpx1;fmu4KHe_58w7<&Hb)tle?wO74Z?T17w zuU-!?-@YD({@w8QoxLB&D07_86q z#3;BXiUSP^&&9DU?l`?Vk>4cpW%|bfkIh6}9Kv0<6k@AQXB_b9x=}2=VmR{5SRY3{ z$K7)wfKFpxzj^iJpOCkCIX7%h=^3{ z=xJ#rQqYB`$UOE6xXYYS+*JL4^-Edvyvm=K-N^s{(UYftv_q;R{wGBQA_5VCh(JUj zBA^g>|0`epDKr3oZI>DVlf1-64lS}YltTgSymuremy;H|8DHY=Prlb^`U!CinS;)s z@*DBU)Zvgs>1we4NP_fvQfQ>4Xav%>!<%O>zIgrW&AY?PcRSGlkf=pup412|J7FP2KO_JrracFR;Fr>p>vM1( z>mP&j9QPPpNnt-+6hN&h<&rTiivpSg+gA}vS^T6Tuq-Gnyao`?#g6t7 zRI`F1+>wN$=ctuuwM0h6f5j?}*{ zAFDI7$tn zPl}o%=J}p7M1D(Vc;TXQCiv*;iQB<_yaIsTpG)Y3D638R*>n|6sdiRQ*)()UxVdOW z0gzX1{MYP1iin&slT5=9p=3jvWD>__G)cj z=G?b7C2M1q&Q)=!j_Fy-?JwXK8%7Ph@BZe$j(d`=8%-+ zgIfX=6t^K9xghMi@(A~zNg?|P^%|(u3s+zd07mgRGx-{?iTgJr^{;;?xPMltIDw?} zs^QX+(@*VvsZhs-Gk~v_hzZ~D8OpVK|8b-bbl%W>wX?Y;egdjFhW*eIWBDyikz z^DC*zY-g0o7Pprr&4=apVIAwej&6WoXcrsQJ2S3;)ib{lxQ{0PQQ&gmrI^=a$VGu> z<4UP8!N_I3-rVL~Tjs;{|7NU@<9De3pTctN6w0K)p`q+BwscN2e17nlyM04&p=RjCjgy&O zmfXK2&T7703ok-F!AIAb=$>dH!S+7+e~OAIT)(|K343Zpk@5(%Ko(LZPP-7cyoSn9 zuxhye%sdR|Y1={EEB{ZjfW1F0(v>5UG(?r^cC5nXV|6A9XAP8qcp0ldFf{f&XFGy> ztN$k>XT6G6aKqscfqkYPc@5_R9S&K#8lG|S*|GW)1Ked!C~gk_pBK-w8XJ(2|Nr|> zp8owk(H!x=B_a?JhzLXkA_5VC00iDAUmy$UJ`Py`>Z?XRP5t8+Rbs4#r!sP_1KNmm8+xZHZ z{j_t+K4A1=XqR$6^H_iF&2!wfx01kqv%yK+dpBGK0O?PASe4DC zsF0Lg)hr|`TJjguXdgi}D;UB(sQ@}W>FH^az(~QJpsXOScrCHT=PcyY>o09V64jFA zF{oDJaAXCiZt=C)?;xNE$g{!rSJIH2S|fFZ%ep1CPL2HaE$+?C*6#k!14z_%KB`FLefr@c(w=b~GQ)|8qH^0H99=Hxbv62+WiQhz0=oCWTx< zF#2dJut=LR70(HJDlADT)o343HOs|wC47qf|0DSS1~pQZ5Grsop%e3h?#TbI>Xf^* zY10JR8#VwmO0Zr`7je2za_-( zL^Y<*tdTpMC+N>46X8sBPqdI=dY}A1UB1xO2ZSz$|K~|P18Mkwr1pz4ig~yXqnfrI z#K`}%5S3zasm3}29$afHLYPW#3%kIAQ~xJ zapFyvSROUlkE@UiOp`+6s{P9aD5p`ywDb#b9-vV!5aj~jhG~GC36FAtTXF#puMP45 z`m0OZc6;s#)el9s;AMzBU>fTmd-ELk*jveOKU^MwDuU*H9V`LmHE@Ab>gS>lps{E` zIta#J$fq1a)&dCA4{MEa@O4s6jrJi_vw|^vs6K!I=8Xg~Za$KZ*oZEGb ztp3&F6Pmuo={_y^4KzeX8l`9G4~;rX&vX$C$w@w#;HPN>{lzOPt*e%jE_%v3muw{X zHxT@Ew9T6W_YZi86!&xo$;j_-;rCgN6$RwDI_J_%OW1nFqzd2Pc5PVoVMnJaTx--h zDSUs4T8ci@*Lz*~{>zN3y{>28vDf#*_e*&{CN&9_`w;bBCzZN{J1~rmfPRDihppix zRfZL?_&j}5ny6i)EA;I$qd z`dBiMTA%NBNTQU6Owvn8!zQ;{Pub^k)(2y6HElbHk^ld$@&DoV|Mc-t+ZZmtBAnw4&;FGu8YljzM+71Q5rK%nr;fn;^w*9v>B$jcFD?74fX@4c~WTLw0nY|a?Osm&L#Q7tfB6Q ze4$G6Mt(o?`;p&&_4eK8Z-%$lj9E(WPgHdsn*KcGM-l=}b%x3~r1j|_>F2tr4qb7o znyT-H(=bStMn?H`SDl2!hakljik>u zc`gdSKaSO(nCIeUj(h5@d2(FpSPQTJFS&Opt(@--8e&6Q zFE-6YFV_|ByBd>(iOxy8nzq*p1%s%-J z0H{akR{{TD;~Na0<8jup1Hgv*ehAcpkUPmxeL@|G!L2$&$%Gf;UO%0zE31<2Oq5Pf zC1eLwuad#h9LUvmTBK0AfK#}68L2G9Y3 zt@p3d5tWP1K6-)=r?BC6;&wD2Px$93lwobLrjhHUs$GT*zMnF|kTw-C_&7V$c5cR0 zibCmIB@!2nnygSYT7M+#Q_XVmT#1(=-~R}{pAK1NM#;LQ7{sB;4Q3+WzsmPhu-aw` zP8#?m(0{t~QZv!-zfSLX=v57JnF`-(MhWmh_grAcXJ7 z360(qi(KJ6!9UNOiSCIO5?k+-@5ehn$Zy>c__URpfrPlvz;a1hH^~Fca34lBZ99mO z@8@FunCtyHBS{=oyzv@|U!}l8$I=aPOJMzfn^$7|Xt%X+`4!oq>kx(d|7onh6z4gi zEAfi|Z=R>+$p8O?Cr|&ur%s{7KYv6ZA`lUX2t))vLImFb@@o_T)h7)r@xtt2 zVc@2DqXHl*0HOlmqgDV=u%Pllq5xlIXPB+?+hikuQyMYKTWOGi%O88l2n3aHWgBOwLUtY8TDBmh8g zLh3PT>48gTG3|(Q{wIla63VT`q7;l>fJvw$y|BItD>t+7`gA8HU#xO$hR^5!sgR!G z8cTuMnV~;4?4W1}>`WKxkZk3nl>pQ|194(77Ni|eqYz+&5deW96>p|)4*nTu3J1#Q z1h7SZmO-I*cnHIlk@;r7x>#~R!bJzwAyUCeb!aUuEZCf*)fV|s3Yaol0$lHP(GFZ@ zTJ^~BaG!9alq+~7=DY`OVI=5drMj6ncbZ*sIY6$+O!f4{x%x={fG63yKE0A%-{4UcY(uPeo#Uy}{I{iuZ7Z&Rw8i1qDD2u`2ES9wP1KuMSeHCTUS zc!tZQ&=_j}LI7A8dN;SUO2+t=UIq@rQ3w!)08t3AMUko&E0H1^mtq<*)Q zOq<90r`|lrJ@r-+*pC(hWSJ@L!sEo0du37}sVfa1 zsAdIYxGN<QE!0=-U8t{zhgj;ek729w`Ww)snL*+wn!3fR^6Y{^3f-?SZu90AXbvI*XP;QzlF4nx-;db~W2C8Cc5bkZ^FkJXS;K6Iia8vE5_QytRN z2@CIY-5vX(I@HIzOg<<+#rK@VIT4QeJK6xy?Xo)DQASrHD)E!$k@DX}`R5%ah)te| z)r|baJvKR2vF4&##klcTQ?L6RSr@9}8l0L9Xr6$}x6ecB~*Z2zQf0^Lpt0!)UcL$_DE@n-c z(3gj^854JR*+Tk@HY~t04jLc2jYsFaXt@x0CsrN5q*0sf0PJY}(PRf;XSsN;#7vR& ze+21|U@=drGoUL6KjBAbKSt7jo%Cm0(RW8Ki$*N6+}I^@{-?X_08o8(9lV(oI|+T5_HV|8FPg&*VUJCb~yjNRYi#{+|Fx zVv$>tl#$S=oxToGq9{b_kKlcSbm>F#VFxE`F!cFPI{Zj&SJADS5k4G$S0`t8gEIzz<##N$m zlH8dZtv`~7x0)4<;hwmEz~XM8I=v>i`E3bsF83(LF(^i(6WOj=rs5hzbZFgrWXyBlmTK{)IF4)Qz4kEqmQKk#JCGf z6I_kmNF1mc`a`qT^~`Ss?z4XQ;*~{MYIU3R#3SHbo`*IgI4_Qe^xPb=Y3a+lrYTQV z^!eeKpPF{a+H=JEgbJ7X%t#9kf`EwPEeK_9@pchXIPwh8kBEn&-~ zZ_H9JGG~Ghr=GYS&Bs&q8ILF_(N9o;P=1MzxU)=4xT%VsGq>+&j$Ytz#483I%)5?2 zg;Lzb<`QbOkEojE0=g38{A{`UH($Pd`QoM5ou{rlggRw|u?DRW5G^=3=Au&LZoauX zMJ}QXTcy^}V-L&?G!65?n})l-m<2j3U@xiNp7Lx!}DW?Fe) z9Bx}Z zMI&Pl#c_oGWjPKRxgD%muZNd!Uk^k7Zg~67o`C1JJvv$~F@r8P zNOPCxWOB57f`s2l2zc@=y!w#bT_*VOIup0!yAQsfo_$==MbdE3h%uQP5m7>&2Su00 z;9ok*DIo>FbJn0MTX^{B;XbTt+ICR)%Kx)IabvxGV}lqLAj;cAdHZXiWZPfH>JMb! zt2x^deEIJD6MKAeZT9u4Yx};X%WT^B8TSu60@YI54e1G|Kz3u=m8b4RHX+T^@|d0o z?CFYk;2gc<_;l|Y&=4uk?JJrERdd{G;F!jGUJS+U*el>Jb3$=b{D1vRY5Ke@pBIz= z|37*1^q+iWR7d>JjR-^pA_5VCh(JW(3W4{3?k!4yU)-e#rFJg`pm3&36AYbkWae$} zC6A*NAW8wE6yWhv0DK#%M@wLiNPL#CaW1t@`MEf=2fW1L`?1PbsdIRK-nbv~1sKQr z$KX82JqB0u=8x6}biDNa65$PkfC|#u$pX;{6%$ASQiCH23vAl5jddHb; zqSXiJ56qIZI)OWu<1Sc#tSCeTlAuB{KXI33hCfAK29gJ;%HaOQ(e{ET)IH*!ibSQm zn+-8T#CVx;g{z+Vjj-Jv>5u(O02u>^595zrn6AEryXWA~5!+tF=}hq9)DyR(`FPU5 zz%(UqXmwD)=J69zJt#XKN>aPF4H4zxdCR*D%anPMVj072#Eo=~sT%Dgs%E)>t^_YT zApNn=s383TpryM3B%Om}-sZJ9;@!T4K4H?I=!^CcFoHs28~SrmXfw*bfzp3LOQ5!;QxsM z5KOXo?G4TekrR)n#+pH&fzu6snV>(Dz{r{Co{%B2`abx7mv5Dp>48ED3i!(mMf?n; z5cgt%<~?gh80XrMDhK_3_a#Vx$M<%;_nG_oG?4GAbor>!iL_Osl z74KpY+Tu4XKca8x*pbWeZNhyzWGP%c|S^-{EHN{(J;>yL z;`C{(j~vw()13POw94hGI8?{<%tiyD(~0uI^Pw3ioEh395}BV3Q4ig9TW~{*Z;S)y zT`YCx?y4H!e&Z+~vvKrxyk0+^sCU;|DY;sXw3~TZ(ziSbHDWkxN<%kqFB;S?V}0D_ zIWBf9sqIJe@)W|#ebFWz87UTxcdXR~g_ia0i=!2fmbwyGM*irAU!|2R)wZ3_M+<^# zv=5=06^!97DEcX*NHsNOyxbG!2fn(BIgrK|p$=jSR24rVjp6?-wtS;n4#JW%?|N55 z$+WoY7Oi|QeSDpnF2W({#Rp3Pa_Mhdldq+k8w#6}Qzy>ZCI+a+OEo|YAR~PsKY9lM z3V>k~5}L`AUgc*llpDnWxsDhuwXY#F2d9VuWTgJ!J0S*O%i)xTaWH{4Kco%kpyIaZ z^hEpfT*}aIRkzBTrc*k>=6sS$4=&n7p%PoTjR@7|8CSsSnRo2=S!vT#FlaNK7eYKb8r2Kq!3W;>WQDX9#y`j+ecf;KQpYZb$d=>HzA!xr!;% zMEGO=c2+HB0hECo!|EHWw!{@zjuC|^2HFJ`;zCJk)C5vBT7M)5ftuywxe`I`KpcPy zLW*5Wsa!0OAG&wGShdrwJYe%{UFZUE6n?vC3S{FY_M=yCe(Tp>^#i@qL$2{XcmNNO)vTNg?G5L z;IR{Ybe)OY;oYYmfIn1O+|B@v1^p=swRwXZVxJS$^EQ)1r;Vq%#nzSYj7Qn2MkH9GxQ`tGNk>@1Utqad*HQ0}A=1HNE&HicrO8NpP zP$=ZD#LU`Fi$|J2ydED8x|0s*HWwZj|q0xaWFm&SRlzxd`k?m4)UwSF}3U-HPuS+PpM zj~}Erfx1bc{e#Mhu>6(sS8;%`o0p`nTun<>wraGGpqdp7;ht!J9I1sxO)^{xP5* zR-LF|rPlh0&wYA2p-wy3-LW64Lw&5WJFFl%I4Fv(DfyzJPJZU*c%y{i3n7ty^~K9) zIq0r7AucG`BrBNEJOS@Z81U(`w^EpGegdDkgdnddN~$aL7`W%lKW^ZOgn%PEENL!u zBlhI-J6v`4B zB9eu(nei1R0J;R|qwB-enc%~#CvHdg@e+a>eI z0@(8z(~dp^Wz{aZ-Kfz%plX(j=Snd4vy~7GKYsCU5ZioJh&(MNxqH>B;-~tLH+`yq z>0?#^&}Fc#68SRy<6a-grRZ>E@067diOKjUr~qgwFR$oAK|iN96ZdTAG>Za&Q=Q%s z9pZPHmJJCuG8ahs+&CWNH;gN8pJk0$biwFL^21)hLgOQ)!n$GMhR_rI-R(?tPqdI| zd#C*W7XN>;n(EymL_VUIWf;W|U>xqlsHSZPG4lW4IsQMi{$HAzv19Evrv4wfm9Gbv ziy8iZ8tX5;c~0ocyW;=n`v2d2^7J=9VRk0|(IWy8frvmvAR_S7MBwXR{OaHM-jjd! zr+3NvqgEt&S0#9fVOB=NR`;og34ev2cKpSacQTzx;j?LdJBKq6`1m(UdPWB=63(c> z`XiJ7zf1~^XLir}^X#K-HC0+-R7Qcd`*F}jao6HX8v47s|+lh+~r*jVRG}fcB=RZb^aF# zcQTh=sc}7_#L@-?D{ERL0CH|RQWv&^xW{ph&Sx z;=ZpSuPH{YTNhQ40B`^YRBKBx^=Z2ZNHI*2NrR{nNv|$j>e}EbLReMn4ZvzLB}(fp z1-_Fm0vs4*w#BU-FXAR|UG{$UfSC>DgvfxYiPyR|F%dvkK)CM(PGj;?&A8g^ zdgdLQeLo5SLUKpshA4`|_~uazfsYLqHQt_SKq^uqe&IBJ$82`(}?Pxxp@rN0s zw?&ok&-k|kD!P-7+dbe);x0m{l1Z_=-Sfn^yhuA~QlL>2NY!ZlkwpD!mW$^~s1+Ii z&pMhZmsQhaUxp7vnze@vZCz9Kd0!r~Gd@MXMC#VcmbCAx>Q3MP> z!0+qV58;9_IFhb379O&lQVt@u9pU@KZrx9l@27iWS|{>l{GXemklxvZ+PC9$X#fxl z{x1yxFg-*k-X;7ae{&Y871@vv%I$k-CAb#)o|Z}+?DJ7ZUJc2m z$jFWV|EFBh#{`%g7ku$7UMKle{3_2 zV_eMR66{8eot*m~&r-@3L*&k+2U?mBNQ6#+GbV3|ut+8F9M!>)gh^_!{>ZSmmr0>< z((ZKyH7Gy@5i^RtHXNe!zKptpZ^=yH6W0;AY3`^SAXB+o$BHo{yhtLp?07Ga9H~kJ zQNKpLUx1SZ(h2Ikb&)kCv1kzr_(gr#$vHTW)t{K>;$@C|46bCeA1xf9zl{7TbKG1P zE)pEE(~#yf&<5CpQ&2eHBRtX@E~#>XvQndc1l6oy2=^o*sJWEeGU-xUFW^Fh5`_@w z_a)4Mu)3KrAnc;&eRR3kB3uj0^^1)O;p-o#rRtd3M z;ajl4^i$1oTRRz<{xYq^5O=o5b)k$*{}v()n-DmFyufD)$t12`(+afC35Xx?ypV47 zau2Oi4`_m+8F0T>n;kBLUhf?(@2@W&!~ye+tIe)w-m%&DL-mVTlFK-uP`^NhW)&z_ zhDD33GDJ5eOgM4n|k|Cen0&EO_tG&Q? z&{@FU$59b*OA+AVMNPW+IgVR(WvvORkAb_&YKs7XUIYtz@p-I&>dkZ9Q*R}u{cr&Q z4M<7-=hV^Am%XFh!#UPa0JK%Wy#c46qP39XLAzPu@+n&F{LZ&>aj4NgglbkWhI>)~ z00VXT^tsPkd<5!>IO2bmSVEC4xb~o`z;p2Csxr-pF*z(7j;bZH} zbkPk-P(D}?P^2`bMqE)-c-oeHbysSv{SU9^EHIv;13&`zjLtFhH+@tDY*7TT+hGhS z@I-5awS1Gi>yp@+g2E6Zb;IvdkXYngVwc@+czqW8l9{?Q<7&6-nRo2={U`!>&U7r& zq=2|bWtO@CV_%PjMS$nWs0bj}n<2%r_!?rLmd3@>MQ;=V(*z%0J#jm_k5>dpYui=~ zE6`J*Xt8eXlgt9aGu>=L%92G8kmeY{&zM5(H+b)g;!UF_H@_OKKawY-n&slT5sDNuLm%OY4f#U zh<%#Ea9U`nMi{w)rs4WCfl*D{4r0^;fZ)fh2XON_o9h3|R7-26mOmr!xVZR=ajd`e z<~gA&Z-M{M)3N^lZ$EkZx7S#O_*q0CA`lUX2t))T0zW$h-v5=a{?+e2`IDd9C7Vxj z(b%<tq?kbBi>R;}to@NGtRf>=reNwCDQ`B z1?hgR9*&XL|1hmj%$#B%%o-FSNK0vh10Cc-|@)1ilo(sf@J{_n4S^5^%{(%wzqtaGv9yg)8amhm-w~Cr}*?t*Sfv zBuJ|Xb=JZ4qx`yknY1;`09LMwMS-<9&esbhiW==BsAdI2xF@c^p%l2JfpR53@YStV z5SqU#iDUjZiDg`*p-aRJcDid;wzlE(DgMXsUR~W%*v~>Y{S7s!vS6JV`a>hj(=%Pn zLsF6tR{%hyHYqm5@`L>CplTEV(G%cTU%Y&l-;zlcMTKVvMb;SQLZEt-Y7_u$5dcWX znF@DoS~=q>e{BdrCv@Z%<5FGO>O$t1ZOav=E++B*&k0cJZ@-kdR5Px&x}JH*R^Jc( zkKPBlRF`yEzsgU#2}|V&Kp*Yt@Ld7e@c%;ah`Skxn6faT@r7ZKmz9Rl-2KA;+X?zZ zIZE}!?eIRH|3{aSrFDaWHfeD#Z+Qu-V1RA_*ieLAQBe9BHuU0_Z9x?(AC7nlQ#IO0 zRLyb$U5Tio0N@b<06NBr1U`ZKH$QRX$jJY%^8ZE_ng|!wAULLrle6z4|9>Chcy8%BO4_ETX+N)J1O;#?EkyNhu50exs@rsz&rY& zQ{5pHiOyhesQt(KU)Wm?u+b|{81DX${D-9q{1!l%ecH|9<#&ys=mDnDK7?vkFot{514!d@*wn=EDnhG( z2$9GCF40F#@4*xmALvpp0CuJec}Rxx(Q*K3mR@#=QL?1$cJ`~<|4|OG!4RM#%UX4bZ4B-mrBM&C9Tq;8R02)rZ+d3;F2!NWcrM}jltfL?Rx6%)F2*7h>oN=|;jUZr}dBH8g&a(`R6jJB0tACio|sGtoWMLZa)v^8e^oZfor#|Nl7>+|%nX zzdxxI-I%4Ui_dro-uYC8!n%e z`E{7bTBVr57uXu$L^ELKgl^vj{y(cm{{N4kJpJR1JVYEG5r_yx1R??vfr!9^5qO_` z^{*fb=yoXz$f`Q0Tq9}at9*fZ(a%a!j+uGY$SDV zcx{mYw2=Pusb}bXXxZyiPb2{2SpU$Q=eUR7N(Ore5`Z$N@j` zHHV_THiXP?5EbwnYMkWW5DCCET7M+>hMElLjo6l z8`qTor^Ur`dF47bd_SHR)VI#a8or;UnJ*Sz^Wo4Ic>W&x?s)98X5ioG*py?Jg;NJ*X?Zp@-8nmC z`GH0q{qb~Yn;|<5{60T;_4eJCE1KGTil1|=)NuhINOVt7a*IIS)zihave$*~r_3LN z7I+GKiW<5<6AOTabJUq}g{#p0cIG$2_E|rC@yY^B4=mQssmZEyc^+Cys@mdsNYBlY zo*X?_z#+#`tIrR|{M58V)}FIIKMr+yIt@cURCfTJ^LD}Y!+bSg+=^L>JsTWYZgUdC z|5xH$lkv^401N+5?P#@xuQS2NS5Mpy@8kLZu0;Ax|Lm&9;(*>q?rDkufNnma0suzq zidm?9X+JF8lr(B`42S?=8m&K)gFwx4@mvX?q5$9#0sxvr;9!qDq(dN219tcJhys9B z0RTZ^N*+N(OKdOBHaC$F1pxO607@z~_#c)OUTd@kTSM6BHk4j?+#(B68 zqnfrI#903?i}Vxa|4oZHyZYe&+1TP8r5D(^hvEOtSpA9Y;CW8y%DdwK^XFw2`Tqw~ zH}U^IA`lUX2t))T0(T<-|NjfS<^QV^4|bAR2@PFoDJN|Gz4U*@vz`~djhlr2hp$*D zEflT5G}sTE=1HM})9&g23c{vM02vd~Oe_{0gd_hS`Tycr52yBoPhi!btAQGsbDIuX z)~7?qrcA|@c`Lc#@!8Q6;FmvJ&931?-%!tPOOvd_y6vd2Ne@7#V?G8Ax1Tm2=nz1q zJ7fJ*aGv9yf-4E_N9zGvI)lSuB{ZSN`^4k2wx|KLqPZVTgQ|db-rM+L_@)>dbUO z4M|EqSOS210&N!BglrAnWmv4vB^)IHTO9KA>uri|0zb6a@f}5CBNy zBkWDg5AxN03r7LK>Jx>lprMYSN)cz}(ud1A%p(7PAOGLrh1ujO50?~Kg=z7k5@0k3 z5Qn&%XPMCXq~< z5+_CkA_5VCh(JW((Fnl*|I54O|Is(o)7o@)hInbGm(E0cQL#gbOMv|u*>$qP#yriw zeuT-VBx%9OSq=7IKJ%o|xM}zFe@&9CN@&kY#avfrnVT((^naxPBmGa|95Bbp~)6W!zAJY0nm0$sNQ60MCR5exK4U`8`a7OtgJu#~jmFU?wmrE2- zC%7}-O#7IUevZgF=>v6NkrH1hRhY;6r`|lrJ@r=7*AJ)rGn{*A0FcU8`QqdvH}L(~ z&zMWXUK_&Pm#ry#)i6oQ48Grv)*s1xTg?iFa94ak&i|OY(m95LA>rk!UnXk#za(-| z$w;{WMj8d&UYEk;Q)*Yx@@HO}!r<31He5t*#zMe#X87yR4QI~+n$t)h$a%(g6kqq}XYXD;`=YnU*53(PxpvJOOZMnk3KD+==#1dkH@;iSi3EUIzP7UK#!89I^1zg7KMlz zrTBLeGdkNolK)%Ce{7uL9_19G$k2{f>ojtbyU9KBBK^Ng z|I@0h?#cu{8RZ?138^Yq!C~b8@8kcu?_@D1Qss+1qvuJ{NJ)$wNP_gr9U=TbMnY3I2)ZOmxq*kZ^ma{C~a0|K9=96!O!kVc}72!B0x}b`DH9`>Y{{Ha~CR>Ium+=IZE2 z;vzLzeXT#>qKiMe;Z_WrPhU=cLyePMEo!v>=%N7_ zX9Z)pCk;T==4~r{6%0M4NW>Wbx5OD9X@vqp+AdF3*DD!GL+F#(EHYJ+Bt$;nv>fl< zYiykvKDy3K7vGSa1YpaZ zjg2$$VnR=6^g80D?k-Ey0svb7_GX9Xr$033ri``6s4)5TjD}BLxQd(^SDRhWykoQP zM*~m_z*(Nq#<`}wS?e1Bkf=o^ag*K=2>^OP@l2TUMW)tKy-w$r>$B9E;KQpYZb$d= z5&-I4<$uZeL+eSJap^A*02D+-H%K!`{WM1`pdOWw`Np1Q;d)Y|eL&SL7tfXODM|ny zApt<$(W399-8zeePDk#67Zm`j3IJraZ4FT^ZR$JPML97{Cv@fc{D0a#PbdEW51%~!!*#|Xei{*o2t))T0uh0Tz+(}3|Che{ zGqC@^xl8t+@(TKyHjoT7X>55$y6nQ1dVdPRaH5-EJ23kN`LJaL zllFoAr?@Y~8JLFc{63)10cOZ+Ve)ZpUU)7F`#+BLkHLA4dkn6muOH3+ca16jBaw^a zzj2dSwMZl?N%BrhawXXVo6qmk3eFZj;r^w{otL^sY2yCvX#J7p{-;^N2=0mdFFG2h T!%x$uzO8Xp=N2W3`G5X@A5s6M diff --git a/entrypoint.sh b/entrypoint.sh deleted file mode 100644 index 2008cdb..0000000 --- a/entrypoint.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash - -cat << EOF -Welcome to - - ###### ###### #### ### ### #### ######### ###### ######### - ### ### ### ### ### ### ### ### ### ### ### ### - ### ### ### ### ### ###### #### ### ### ### ### - ### ### ### ### ### ### ### #### ### ############ ### - ### ### ### ### ### ### ### #### ### ### ### ### - ###### ###### #### ### ### #### ### ### ### ### (API) - -Useful links: - -- Documentation: https://outline.itsnik.de/s/dockstat -- GitHub (Frontend): https://github.com/its4nik/dockstat -- GitHub (Backend): https://github.com/its4nik/dockstatapi -- API Documentation: http://localhost:7000/api-docs - -Summary: - -DockStat and DockStatAPI are 2 fully OpenSource projects, DockStatAPI is a simple but extensible API which allows queries via a REST endpoint. - -EOF - -npm run start diff --git a/environment.d.ts b/environment.d.ts new file mode 100644 index 0000000..803ae43 --- /dev/null +++ b/environment.d.ts @@ -0,0 +1,44 @@ +declare global { + namespace NodeJS { + interface ProcessEnv { + // Node specific: + NODE_ENV: "development" | "production"; + TRUSTED_PROXYS: string | undefined; + + // User.conf + RUNNING_IN_DOCKER: string | undefined; + VERSION: string | undefined; + + // High Availability + HA_MASTER: string | undefined; //bool + HA_MASTER_IP: string | undefined; + HA_NODE: string | undefined; //ip list with port seperated by "," like: "10.0.0.4:5012,10.0.0.5:9876" + HA_UNSAFE: string | undefined; + + // Notification services: + DISCORD_WEBHOOK_URL: string | undefined; + + EMAIL_SENDER: string | undefined; + EMAIL_RECIPIENT: string | undefined; + EMAIL_PASSWORD: string | undefined; + EMAIL_SERVICE: string | undefined; + + PUSHBULLET_ACCESS_TOKEN: string | undefined; + + PUSHOVER_USER_KEY: string | undefined; + PUSHOVER_API_TOKEN: string | undefined; + + SLACK_WEBHOOK_URL: string | undefined; + + TELEGRAM_BOT_TOKEN: string | undefined; + TELEGRAM_CHAT_ID: string | undefined; + + WHATSAPP_API_URL: string | undefined; + WHATSAPP_RECIPIENT: string | undefined; + + CUSTOM_NOTIFICATION: string | undefined; // enter the script name without .js here and without custom/... + } + } +} + +export {}; diff --git a/middleware/authMiddleware.js b/middleware/authMiddleware.js deleted file mode 100644 index 8ee6a68..0000000 --- a/middleware/authMiddleware.js +++ /dev/null @@ -1,50 +0,0 @@ -const bcrypt = require("bcrypt"); -const fs = require("fs"); -const path = require("path"); -const logger = require("../utils/logger"); -const passwordFile = path.join(__dirname, "password.json"); -const passwordBool = path.join(__dirname, "usePassword.txt"); - -function authMiddleware(req, res, next) { - fs.readFile(passwordBool, "utf8", (err, data) => { - if (err) { - logger.error("Error reading the file:", err); - return; - } - - const isAuthEnabled = data.trim() === "true"; - - if (!isAuthEnabled) { - return next(); - } - - const providedPassword = req.headers["x-password"]; - if (!providedPassword) { - logger.error("Password required - Denied"); - return res.status(401).json({ message: "Password required" }); - } - - fs.readFile(passwordFile, "utf8", (err, data) => { - if (err) { - logger.error("Error reading password"); - return res.status(500).json({ message: "Error reading password" }); - } - - const storedData = JSON.parse(data); - bcrypt.compare(providedPassword, storedData.hash, (err, result) => { - if (err) { - logger.error("Error validating password - Denied access"); - return res.status(500).json({ message: "Error validating password" }); - } - if (!result) { - console.error("Invalid Password - Denied access"); - return res.status(401).json({ message: "Invalid password" }); - } - - next(); - }); - }); - }); -} - -module.exports = authMiddleware; diff --git a/middleware/password.json b/middleware/password.json deleted file mode 100644 index 37a7c4c..0000000 --- a/middleware/password.json +++ /dev/null @@ -1 +0,0 @@ -{"hash":"$2b$10$qGcNmciEGhX.PiB.ofHib.Fob.nOjQNfguBoD4JDbbbTysrLrKGEi","salt":"$2b$10$qGcNmciEGhX.PiB.ofHib."} \ No newline at end of file diff --git a/misc/dependencyGraphs/mermaid-all.txt b/misc/dependencyGraphs/mermaid-all.txt deleted file mode 100644 index 6036bdd..0000000 --- a/misc/dependencyGraphs/mermaid-all.txt +++ /dev/null @@ -1,106 +0,0 @@ -flowchart LR - -subgraph 0["config"] -1["db.js"] -2["swaggerConfig.js"] -B["dockerConfig.json"] -end -subgraph 3["controllers"] -4["containerController.js"] -7["databaseMigration.js"] -8["fetchData.js"] -C["frontendConfiguration.js"] -D["scheduler.js"] -end -subgraph 5["utils"] -6["dockerClient.js"] -A["containerService.js"] -Q["extractHostData.js"] -R["writeOfflineLog.js"] -subgraph U["notifications"] -V["_notify.js"] -W["discord.js"] -subgraph X["data"] -Y["template.js"] -end -Z["email.js"] -10["pushbullet.js"] -11["pushover.js"] -12["slack.js"] -13["telegram.js"] -14["whatsapp.js"] -end -end -9["child_process"] -subgraph E["middleware"] -F["authMiddleware.js"] -G["rateLimiter.js"] -end -subgraph H["routes"] -subgraph I["auth"] -J["routes.js"] -end -subgraph K["data"] -L["routes.js"] -end -subgraph M["frontendController"] -N["routes.js"] -end -subgraph O["getter"] -P["routes.js"] -end -subgraph S["notifications"] -T["routes.js"] -end -subgraph 15["setter"] -16["routes.js"] -end -end -17["server.js"] -subgraph 18["swagger"] -19["swaggerDocs.js"] -end -4-->6 -7-->1 -8-->1 -8-->A -8-->9 -A-->B -A-->6 -D-->1 -D-->8 -L-->1 -N-->C -P-->B -P-->D -P-->A -P-->6 -P-->Q -P-->R -T-->V -V-->W -V-->Z -V-->10 -V-->11 -V-->12 -V-->13 -V-->14 -W-->Y -Z-->Y -10-->Y -11-->Y -12-->Y -13-->Y -14-->Y -16-->D -17-->D -17-->F -17-->G -17-->J -17-->L -17-->N -17-->P -17-->T -17-->16 -17-->19 -19-->2 diff --git a/misc/dependencyGraphs/mermaid-api.txt b/misc/dependencyGraphs/mermaid-api.txt deleted file mode 100644 index 0ae832b..0000000 --- a/misc/dependencyGraphs/mermaid-api.txt +++ /dev/null @@ -1,35 +0,0 @@ -flowchart LR - -subgraph 0["routes"] -subgraph 1["getter"] -2["routes.js"] -end -end -subgraph 3["config"] -4["dockerConfig.json"] -7["db.js"] -end -subgraph 5["controllers"] -6["scheduler.js"] -8["fetchData.js"] -end -9["child_process"] -subgraph A["utils"] -B["containerService.js"] -C["dockerClient.js"] -D["extractHostData.js"] -E["writeOfflineLog.js"] -end -2-->4 -2-->6 -2-->B -2-->C -2-->D -2-->E -6-->7 -6-->8 -8-->7 -8-->B -8-->9 -B-->4 -B-->C diff --git a/misc/dependencyGraphs/mermaid-conf.txt b/misc/dependencyGraphs/mermaid-conf.txt deleted file mode 100644 index 6e06cc6..0000000 --- a/misc/dependencyGraphs/mermaid-conf.txt +++ /dev/null @@ -1,28 +0,0 @@ -flowchart LR - -subgraph 0["routes"] -subgraph 1["setter"] -2["routes.js"] -end -end -subgraph 3["controllers"] -4["scheduler.js"] -7["fetchData.js"] -end -subgraph 5["config"] -6["db.js"] -B["dockerConfig.json"] -end -8["child_process"] -subgraph 9["utils"] -A["containerService.js"] -C["dockerClient.js"] -end -2-->4 -4-->6 -4-->7 -7-->6 -7-->A -7-->8 -A-->B -A-->C diff --git a/misc/dependencyGraphs/mermaid-notificationService.txt b/misc/dependencyGraphs/mermaid-notificationService.txt deleted file mode 100644 index dbfbd46..0000000 --- a/misc/dependencyGraphs/mermaid-notificationService.txt +++ /dev/null @@ -1,37 +0,0 @@ -flowchart LR - -subgraph 0["routes"] -subgraph 1["notifications"] -2["routes.js"] -end -end -subgraph 3["utils"] -subgraph 4["notifications"] -5["_notify.js"] -6["discord.js"] -subgraph 7["data"] -8["template.js"] -end -9["email.js"] -A["pushbullet.js"] -B["pushover.js"] -C["slack.js"] -D["telegram.js"] -E["whatsapp.js"] -end -end -2-->5 -5-->6 -5-->9 -5-->A -5-->B -5-->C -5-->D -5-->E -6-->8 -9-->8 -A-->8 -B-->8 -C-->8 -D-->8 -E-->8 diff --git a/misc/entrypoint.sh b/misc/entrypoint.sh deleted file mode 100755 index 2008cdb..0000000 --- a/misc/entrypoint.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash - -cat << EOF -Welcome to - - ###### ###### #### ### ### #### ######### ###### ######### - ### ### ### ### ### ### ### ### ### ### ### ### - ### ### ### ### ### ###### #### ### ### ### ### - ### ### ### ### ### ### ### #### ### ############ ### - ### ### ### ### ### ### ### #### ### ### ### ### - ###### ###### #### ### ### #### ### ### ### ### (API) - -Useful links: - -- Documentation: https://outline.itsnik.de/s/dockstat -- GitHub (Frontend): https://github.com/its4nik/dockstat -- GitHub (Backend): https://github.com/its4nik/dockstatapi -- API Documentation: http://localhost:7000/api-docs - -Summary: - -DockStat and DockStatAPI are 2 fully OpenSource projects, DockStatAPI is a simple but extensible API which allows queries via a REST endpoint. - -EOF - -npm run start diff --git a/nodemon.json b/nodemon.json new file mode 100644 index 0000000..30602eb --- /dev/null +++ b/nodemon.json @@ -0,0 +1,6 @@ +{ + "ignore": ["src/logs", "**/fixtures/**", ".gitignore", "**/*.json"], + "execMap": { + "ts": "tsx" + } +} diff --git a/package-lock.json b/package-lock.json index f4dcffb..641c0d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,26 +9,44 @@ "version": "2", "license": "BSD 3-Clause License", "dependencies": { + "@types/dockerode": "^3.3.31", + "@types/supports-color": "^8.1.3", + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.7", "bcrypt": "^5.1.1", - "child_process": "^1.0.2", + "chokidar": "^4.0.1", "cors": "^2.8.5", "dockerode": "^4.0.2", "express": "^4.21.1", "express-rate-limit": "^7.4.1", - "js-yaml": "^4.1.0", + "https": "^1.0.0", + "ipaddr.js": "^2.2.0", "node-fetch": "^3.3.2", "nodemailer": "^6.9.16", - "python-shell": "^5.0.0", "sqlite3": "^5.1.7", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1", - "twilio": "^5.3.5", - "winston": "^3.15.0", - "yamljs": "^0.3.0" + "winston": "^3.15.0" }, "devDependencies": { + "@playwright/test": "^1.49.0", + "@types/bcrypt": "^5.0.2", + "@types/cors": "^2.8.17", + "@types/express": "^5.0.0", + "@types/express-handlebars": "^5.3.1", + "@types/node": "^22.9.0", + "@types/node-fetch": "^2.6.12", + "@types/nodemailer": "^6.4.17", "dependency-cruiser": "^16.5.0", - "nodemon": "^3.1.7" + "nodemon": "^3.1.7", + "ora": "^8.1.1", + "pkg": "^5.8.1", + "ts-node": "^10.9.2", + "tsx": "^4.19.2", + "uglify-js": "^3.19.3" + }, + "engines": { + "npm": ">=10.8.2" } }, "node_modules/@apidevtools/json-schema-ref-parser": { @@ -75,6 +93,69 @@ "openapi-types": ">=7" } }, + "node_modules/@babel/generator": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.2.tgz", + "integrity": "sha512-W1lG5vUwFvfMd8HVXqdfbuG7RuaSrTCCD8cl8fP8wOivdbtbIg2Db3IWUcgvfxKbbn6ZBGYRW/Zk1MIwK49mgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.18.2", + "@jridgewell/gen-mapping": "^0.3.0", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.4.tgz", + "integrity": "sha512-FDge0dFazETFcxGw/EXzOkN8uJp0PC7Qbm+Pe9T+av2zlBpOgunFHkQPPn+eRuClU73JF+98D531UgayY89tow==", + "dev": true, + "license": "MIT", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.19.0.tgz", + "integrity": "sha512-YuGopBq3ke25BVSiS6fgF49Ul9gH1x70Bcr6bqRLjWCkcX8Hre1/5+z+IiWOIerRMSSEfGZVB9z9kyq7wVs9YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.18.10", + "@babel/helper-validator-identifier": "^7.18.6", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@balena/dockerignore": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz", @@ -90,6 +171,19 @@ "node": ">=0.1.90" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@dabh/diagnostics": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", @@ -101,199 +195,924 @@ "kuler": "^2.0.0" } }, - "node_modules/@gar/promisify": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", - "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", + "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", + "cpu": [ + "ppc64" + ], + "dev": true, "license": "MIT", - "optional": true - }, - "node_modules/@jsdevtools/ono": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", - "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", - "license": "MIT" - }, - "node_modules/@mapbox/node-pre-gyp": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", - "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", - "license": "BSD-3-Clause", - "dependencies": { - "detect-libc": "^2.0.0", - "https-proxy-agent": "^5.0.0", - "make-dir": "^3.1.0", - "node-fetch": "^2.6.7", - "nopt": "^5.0.0", - "npmlog": "^5.0.1", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.11" - }, - "bin": { - "node-pre-gyp": "bin/node-pre-gyp" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/are-we-there-yet": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", - "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": ">=10" + "node": ">=18" } }, - "node_modules/@mapbox/node-pre-gyp/node_modules/gauge": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", - "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.2", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.1", - "object-assign": "^4.1.1", - "signal-exit": "^3.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.2" - }, + "node_modules/@esbuild/android-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", + "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=10" + "node": ">=18" } }, - "node_modules/@mapbox/node-pre-gyp/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "node_modules/@esbuild/android-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", + "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } + "node": ">=18" } }, - "node_modules/@mapbox/node-pre-gyp/node_modules/npmlog": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", - "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "dependencies": { - "are-we-there-yet": "^2.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^3.0.0", - "set-blocking": "^2.0.0" + "node_modules/@esbuild/android-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", + "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@npmcli/fs": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", - "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", - "license": "ISC", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", + "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", "optional": true, - "dependencies": { - "@gar/promisify": "^1.0.1", - "semver": "^7.3.5" + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@npmcli/move-file": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", - "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", - "deprecated": "This functionality has been moved to @npmcli/fs", + "node_modules/@esbuild/darwin-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", + "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2" - }, + "os": [ + "darwin" + ], "engines": { - "node": ">=10" + "node": ">=18" } }, - "node_modules/@tootallnate/once": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", + "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">= 6" + "node": ">=18" } }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "license": "MIT" - }, - "node_modules/@types/triple-beam": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", - "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", - "license": "MIT" - }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "license": "ISC" - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", + "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">= 0.6" + "node": ">=18" } }, - "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "node_modules/@esbuild/linux-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", + "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=0.4.0" + "node": ">=18" } }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "node_modules/@esbuild/linux-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", + "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/acorn-jsx-walk": { - "version": "2.0.0", + "node_modules/@esbuild/linux-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", + "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", + "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", + "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", + "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", + "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", + "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", + "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", + "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", + "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", + "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", + "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", + "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", + "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", + "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "license": "MIT", + "optional": true + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "license": "MIT" + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "license": "MIT", + "optional": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@playwright/test": { + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0.tgz", + "integrity": "sha512-DMulbwQURa8rNIQrf94+jPJQ4FmOVdpE5ZppRNvWVjvhC+6sOeo28r8MgIpQRYouXRtt/FCCXU7zn20jnHR4Qw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.49.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/bcrypt": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", + "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/docker-modem": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz", + "integrity": "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/ssh2": "*" + } + }, + "node_modules/@types/dockerode": { + "version": "3.3.32", + "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.32.tgz", + "integrity": "sha512-xxcG0g5AWKtNyh7I7wswLdFvym4Mlqks5ZlKzxEUrGHS0r0PUOfxm2T0mspwu10mHQqu3Ck3MI3V2HqvLWE1fg==", + "license": "MIT", + "dependencies": { + "@types/docker-modem": "*", + "@types/node": "*", + "@types/ssh2": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", + "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-handlebars": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@types/express-handlebars/-/express-handlebars-5.3.1.tgz", + "integrity": "sha512-DSzaERLO4gHb8AqnrL58jzSDyT0yDdl6HqDc+bGz1Hf0nrG1FK30nHGzv8NBEGR8QV9eUGB/YaE0Qj3NjF7siw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.2.tgz", + "integrity": "sha512-vluaspfvWEtE4vcSDlKRNer52DvOGrB2xv6diXy6UKyKW0lqZiWHGNApSyxOv+8DE5Z27IzVvE7hNkxg7EXIcg==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", + "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/nodemailer": { + "version": "6.4.17", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz", + "integrity": "sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.9.17", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz", + "integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==", + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/ssh2": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.1.tgz", + "integrity": "sha512-ZIbEqKAsi5gj35y4P4vkJYly642wIbY6PqoN0xiyQGshKUGXR9WQjF/iF9mXBQ8uBKy3ezfsCkcoHKhd0BzuDA==", + "license": "MIT", + "dependencies": { + "@types/node": "^18.11.18" + } + }, + "node_modules/@types/ssh2/node_modules/@types/node": { + "version": "18.19.67", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.67.tgz", + "integrity": "sha512-wI8uHusga+0ZugNp0Ol/3BqQfEcCCNfojtO6Oou9iVNGPTL6QNSdnUdqq85fRgIorLhLMuPIKpsN98QE9Nh+KQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/ssh2/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, + "node_modules/@types/supports-color": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/@types/supports-color/-/supports-color-8.1.3.tgz", + "integrity": "sha512-Hy6UMpxhE3j1tLpl27exp1XqHD7n8chAiNPzWfz16LPZoMMoSc4dzLl6w9qijkEb/r5O1ozdu1CWGA2L83ZeZg==", + "license": "MIT" + }, + "node_modules/@types/swagger-jsdoc": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.4.tgz", + "integrity": "sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ==", + "license": "MIT" + }, + "node_modules/@types/swagger-ui-express": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.7.tgz", + "integrity": "sha512-ovLM9dNincXkzH4YwyYpll75vhzPBlWx6La89wwvYH7mHjVpf0X0K/vR/aUM7SRxmr5tt9z7E5XJcjQ46q+S3g==", + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-jsx-walk": { + "version": "2.0.0", "resolved": "https://registry.npmjs.org/acorn-jsx-walk/-/acorn-jsx-walk-2.0.0.tgz", "integrity": "sha512-uuo6iJj4D4ygkdzd6jPtcxs8vZgDX9YFIkqczGImoypX2fQ4dVImmu3UzA4ynixCIMTrEOWW+95M2HuBaCEOVA==", "dev": true, @@ -406,6 +1225,26 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/ansi-styles/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ansi-styles/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -420,6 +1259,19 @@ "node": ">= 8" } }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", @@ -427,28 +1279,31 @@ "license": "ISC" }, "node_modules/are-we-there-yet": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", - "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", "deprecated": "This package is no longer supported.", "license": "ISC", - "optional": true, "dependencies": { "delegates": "^1.0.0", "readable-stream": "^3.6.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": ">=10" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" }, "node_modules/array-flatten": { "version": "1.1.1", @@ -456,6 +1311,16 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -475,17 +1340,17 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, "license": "MIT" }, - "node_modules/axios": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", - "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" } }, "node_modules/balanced-match": { @@ -537,12 +1402,6 @@ "tweetnacl": "^0.14.3" } }, - "node_modules/bcrypt/node_modules/node-addon-api": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", - "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", - "license": "MIT" - }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -580,6 +1439,7 @@ "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -603,6 +1463,7 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", "dependencies": { "ms": "2.0.0" } @@ -610,7 +1471,8 @@ "node_modules/body-parser/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" }, "node_modules/brace-expansion": { "version": "1.1.11", @@ -659,12 +1521,6 @@ "ieee754": "^1.1.13" } }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "license": "BSD-3-Clause" - }, "node_modules/buildcheck": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", @@ -678,6 +1534,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -712,26 +1569,16 @@ "node": ">= 10" } }, - "node_modules/cacache/node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "license": "ISC", - "optional": true, - "engines": { - "node": ">=10" - } - }, "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "set-function-length": "^1.2.2" }, "engines": { "node": ">= 0.4" @@ -740,6 +1587,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", + "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/call-me-maybe": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", @@ -747,81 +1607,41 @@ "license": "MIT" }, "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", "dev": true, "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, "engines": { - "node": ">=10" + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, "funding": { "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chalk/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/child_process": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/child_process/-/child_process-1.0.2.tgz", - "integrity": "sha512-Wmza/JzL0SiWz7kl6MhIKT5ceIlnFPJX+lwUGj7Clhy5MMldsSoJR0+uvRzOS5Kv45Mq7t1PoE8TsOA9bzvb6g==", - "license": "ISC" - }, "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", "license": "MIT", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "readdirp": "^4.0.1" }, "engines": { - "node": ">= 8.10.0" + "node": ">= 14.16.0" }, "funding": { "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" } }, "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } }, "node_modules/clean-stack": { "version": "2.2.0", @@ -833,6 +1653,47 @@ "node": ">=6" } }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, "node_modules/color": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", @@ -844,22 +1705,18 @@ } }, "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "license": "MIT", "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" + "color-name": "1.1.3" } }, "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "license": "MIT" }, "node_modules/color-string": { @@ -881,21 +1738,6 @@ "color-support": "bin.js" } }, - "node_modules/color/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "license": "MIT" - }, "node_modules/colorspace": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", @@ -910,6 +1752,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -919,12 +1762,13 @@ } }, "node_modules/commander": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", - "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 6" + "node": ">=18" } }, "node_modules/concat-map": { @@ -955,6 +1799,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -974,6 +1819,13 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -1001,6 +1853,13 @@ "node": ">=10.0.0" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -1010,19 +1869,13 @@ "node": ">= 12" } }, - "node_modules/dayjs": { - "version": "1.11.13", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", - "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", - "license": "MIT" - }, "node_modules/debug": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", - "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -1061,6 +1914,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -1077,6 +1931,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -1092,18 +1947,19 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/dependency-cruiser": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/dependency-cruiser/-/dependency-cruiser-16.5.0.tgz", - "integrity": "sha512-6IELC3qRumlwhnbPLmcOK6WWdiGPFBw9a+D8DUsnTFpZ81tEtkAud4OPmU3OJFcuWS5VpgvKlctFkby5XDsGzQ==", + "version": "16.7.0", + "resolved": "https://registry.npmjs.org/dependency-cruiser/-/dependency-cruiser-16.7.0.tgz", + "integrity": "sha512-522LLjHINl9r0RIZ8/6s6TqIHTuEJG3XDU2WPSm9dG0rvLUYVyQwE9ID31tDFs4OOyEhdOPaqAaAG1jRv/Zwbg==", "dev": true, "license": "MIT", "dependencies": { - "acorn": "^8.13.0", + "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "acorn-jsx-walk": "^2.0.0", "acorn-loose": "^8.4.0", @@ -1123,8 +1979,8 @@ "safe-regex": "^2.1.1", "semver": "^7.6.3", "teamcity-service-messages": "^0.1.14", - "tsconfig-paths-webpack-plugin": "^4.1.0", - "watskeburt": "^4.1.0" + "tsconfig-paths-webpack-plugin": "^4.2.0", + "watskeburt": "^4.1.1" }, "bin": { "depcruise": "bin/dependency-cruise.mjs", @@ -1138,33 +1994,11 @@ "node": "^18.17||>=20" } }, - "node_modules/dependency-cruiser/node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/dependency-cruiser/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", "engines": { "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" @@ -1179,6 +2013,29 @@ "node": ">=8" } }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/docker-modem": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.3.tgz", @@ -1220,19 +2077,25 @@ "node": ">=6.0.0" } }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "license": "Apache-2.0", + "node_modules/dunder-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.0.tgz", + "integrity": "sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A==", + "license": "MIT", "dependencies": { - "safe-buffer": "^5.0.1" + "call-bind-apply-helpers": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" }, "node_modules/emoji-regex": { "version": "8.0.0", @@ -1250,6 +2113,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -1318,12 +2182,10 @@ "optional": true }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -1332,14 +2194,66 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", "engines": { "node": ">= 0.4" } }, + "node_modules/esbuild": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", + "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.23.1", + "@esbuild/android-arm": "0.23.1", + "@esbuild/android-arm64": "0.23.1", + "@esbuild/android-x64": "0.23.1", + "@esbuild/darwin-arm64": "0.23.1", + "@esbuild/darwin-x64": "0.23.1", + "@esbuild/freebsd-arm64": "0.23.1", + "@esbuild/freebsd-x64": "0.23.1", + "@esbuild/linux-arm": "0.23.1", + "@esbuild/linux-arm64": "0.23.1", + "@esbuild/linux-ia32": "0.23.1", + "@esbuild/linux-loong64": "0.23.1", + "@esbuild/linux-mips64el": "0.23.1", + "@esbuild/linux-ppc64": "0.23.1", + "@esbuild/linux-riscv64": "0.23.1", + "@esbuild/linux-s390x": "0.23.1", + "@esbuild/linux-x64": "0.23.1", + "@esbuild/netbsd-x64": "0.23.1", + "@esbuild/openbsd-arm64": "0.23.1", + "@esbuild/openbsd-x64": "0.23.1", + "@esbuild/sunos-x64": "0.23.1", + "@esbuild/win32-arm64": "0.23.1", + "@esbuild/win32-ia32": "0.23.1", + "@esbuild/win32-x64": "0.23.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" }, "node_modules/esutils": { "version": "2.0.3", @@ -1354,6 +2268,7 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -1368,9 +2283,9 @@ } }, "node_modules/express": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "license": "MIT", "dependencies": { "accepts": "~1.3.8", @@ -1392,7 +2307,7 @@ "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", @@ -1407,6 +2322,10 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express-rate-limit": { @@ -1446,6 +2365,23 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, "node_modules/fast-uri": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", @@ -1453,6 +2389,16 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fecha": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", @@ -1505,6 +2451,7 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", @@ -1522,6 +2469,7 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", "dependencies": { "ms": "2.0.0" } @@ -1529,7 +2477,8 @@ "node_modules/finalhandler/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" }, "node_modules/fn.name": { "version": "1.1.0", @@ -1537,30 +2486,11 @@ "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", "license": "MIT" }, - "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/form-data": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -1596,16 +2526,77 @@ "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, + "node_modules/from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "node_modules/from2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/from2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/from2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "license": "MIT" }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -1625,9 +2616,9 @@ "license": "ISC" }, "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1643,41 +2634,69 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/gauge": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", - "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", "deprecated": "This package is no longer supported.", "license": "ISC", - "optional": true, "dependencies": { "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.3", - "console-control-strings": "^1.1.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", "has-unicode": "^2.0.1", - "signal-exit": "^3.0.7", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1", - "wide-align": "^1.1.5" + "wide-align": "^1.1.2" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": ">=10" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.5.tgz", + "integrity": "sha512-Y4+pKa7XeRUPWFNvOOYHkRYrfzW07oraURSvjDmRVOJ748OrVmeXtpE4+GCEHncjCjkTxPNRt8kEbxDhsn6VTg==", + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "dunder-proto": "^1.0.0", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -1686,6 +2705,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-tsconfig": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz", + "integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -1742,22 +2774,44 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/global-directory/node_modules/ini": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", - "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, - "license": "ISC", + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dependencies": { - "get-intrinsic": "^1.1.3" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1770,6 +2824,16 @@ "devOptional": true, "license": "ISC" }, + "node_modules/has": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", + "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -1784,6 +2848,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" }, @@ -1791,21 +2856,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -1823,6 +2878,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -1841,6 +2897,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", @@ -1867,6 +2924,12 @@ "node": ">= 6" } }, + "node_modules/https": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz", + "integrity": "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==", + "license": "ISC" + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -1894,6 +2957,7 @@ "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -1983,10 +3047,14 @@ "license": "ISC" }, "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC" + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } }, "node_modules/interpret": { "version": "3.1.1", @@ -1998,6 +3066,23 @@ "node": ">=10.13.0" } }, + "node_modules/into-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-6.0.0.tgz", + "integrity": "sha512-XHbaOAvP+uFKUFsOgoNPRjLkwB+I22JFPFe5OjTkQ0nwgj6+pSjb4NmB6VMxaPshLiOf+zcpOCBQuLwC1KHhZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "from2": "^2.3.0", + "p-is-promise": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ip-address": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", @@ -2012,20 +3097,13 @@ "node": ">= 12" } }, - "node_modules/ip-address/node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "license": "BSD-3-Clause", - "optional": true - }, "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", "license": "MIT", "engines": { - "node": ">= 0.10" + "node": ">= 10" } }, "node_modules/is-arrayish": { @@ -2112,6 +3190,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-lambda": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", @@ -2154,6 +3245,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2173,12 +3284,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/js-yaml/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, "node_modules/jsbn": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", @@ -2186,6 +3291,19 @@ "license": "MIT", "optional": true }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -2206,47 +3324,17 @@ "node": ">=6" } }, - "node_modules/jsonwebtoken": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", - "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, "license": "MIT", "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.0.0", - "ms": "^2.1.1", - "semver": "^7.5.4" + "universalify": "^2.0.0" }, - "engines": { - "node": ">=12", - "npm": ">=6" - } - }, - "node_modules/jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", - "license": "MIT", - "dependencies": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", - "license": "MIT", - "dependencies": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" + "optionalDependencies": { + "graceful-fs": "^4.1.6" } }, "node_modules/kleur": { @@ -2271,64 +3359,52 @@ "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", "license": "MIT" }, - "node_modules/lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", - "license": "MIT" - }, - "node_modules/lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", - "license": "MIT" - }, "node_modules/lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", "license": "MIT" }, - "node_modules/lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", - "license": "MIT" - }, - "node_modules/lodash.isnumber": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", - "license": "MIT" - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "license": "MIT" - }, - "node_modules/lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", - "license": "MIT" - }, "node_modules/lodash.mergewith": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", "license": "MIT" }, - "node_modules/lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", - "license": "MIT" + "node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/logform": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.6.1.tgz", - "integrity": "sha512-CdaO738xRapbKIMVn2m4F6KTj4j7ooJ8POVnebSgKo3KBz5axNXRAL7ZdRjIV6NOr2Uf4vjtRkxrFETOioCqSA==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", "license": "MIT", "dependencies": { "@colors/colors": "1.6.0", @@ -2379,6 +3455,13 @@ "semver": "bin/semver.js" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, "node_modules/make-fetch-happen": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", @@ -2411,6 +3494,7 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -2435,10 +3519,21 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -2448,10 +3543,38 @@ "node": ">= 0.6" } }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", "bin": { "mime": "cli.js" }, @@ -2640,15 +3763,40 @@ "license": "MIT" }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/multistream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/multistream/-/multistream-4.1.0.tgz", + "integrity": "sha512-J1XDiAmmNpRCBfIWJv+n0ymC4ABcf/Pl+5YvC5B/D2f/2+8PtHvCNxMPKiQcZyi922Hq69J2YOpb1pTywfifyw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "once": "^1.4.0", + "readable-stream": "^3.6.0" + } + }, "node_modules/nan": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", - "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==", + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", + "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", "license": "MIT", "optional": true }, @@ -2680,9 +3828,9 @@ } }, "node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", "license": "MIT" }, "node_modules/node-domexception": { @@ -2747,6 +3895,59 @@ "node": ">= 10.12.0" } }, + "node_modules/node-gyp/node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/node-gyp/node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^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" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/node-gyp/node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/nodemailer": { "version": "6.9.16", "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.16.tgz", @@ -2778,11 +3979,62 @@ "nodemon": "bin/nodemon.js" }, "engines": { - "node": ">=10" + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/nodemon/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nodemon" + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/nodemon/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" } }, "node_modules/nopt": { @@ -2811,20 +4063,16 @@ } }, "node_modules/npmlog": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", - "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", "deprecated": "This package is no longer supported.", "license": "ISC", - "optional": true, "dependencies": { - "are-we-there-yet": "^3.0.0", + "are-we-there-yet": "^2.0.0", "console-control-strings": "^1.1.0", - "gauge": "^4.0.3", + "gauge": "^3.0.0", "set-blocking": "^2.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "node_modules/object-assign": { @@ -2837,9 +4085,10 @@ } }, "node_modules/object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -2851,6 +4100,7 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", "dependencies": { "ee-first": "1.1.1" }, @@ -2873,79 +4123,442 @@ "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", "license": "MIT", "dependencies": { - "fn.name": "1.x.x" + "fn.name": "1.x.x" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "license": "MIT", + "peer": true + }, + "node_modules/ora": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.1.1.tgz", + "integrity": "sha512-YWielGi1XzG1UTvOaCFaNgEnuhZVMSHYkW/FQ7UX8O26PtlpdM84c0f7wLPlkvx2RfiQmnzd61d/MGxmpQeJPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ora/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ora/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/p-is-promise": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-3.0.0.tgz", + "integrity": "sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg": { + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/pkg/-/pkg-5.8.1.tgz", + "integrity": "sha512-CjBWtFStCfIiT4Bde9QpJy0KeH19jCfwZRJqHFDFXfhUklCx8JoFmMj3wgnEYIwGmZVNkhsStPHEOnrtrQhEXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/generator": "7.18.2", + "@babel/parser": "7.18.4", + "@babel/types": "7.19.0", + "chalk": "^4.1.2", + "fs-extra": "^9.1.0", + "globby": "^11.1.0", + "into-stream": "^6.0.0", + "is-core-module": "2.9.0", + "minimist": "^1.2.6", + "multistream": "^4.1.0", + "pkg-fetch": "3.4.2", + "prebuild-install": "7.1.1", + "resolve": "^1.22.0", + "stream-meter": "^1.0.4" + }, + "bin": { + "pkg": "lib-es5/bin.js" + }, + "peerDependencies": { + "node-notifier": ">=9.0.1" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/pkg-fetch": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/pkg-fetch/-/pkg-fetch-3.4.2.tgz", + "integrity": "sha512-0+uijmzYcnhC0hStDjm/cl2VYdrmVVBpe7Q8k9YBojxmR5tG8mvR9/nooQq3QSXiQqORDVOTY3XqMEqJVIzkHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "fs-extra": "^9.1.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.6", + "progress": "^2.0.3", + "semver": "^7.3.5", + "tar-fs": "^2.1.1", + "yargs": "^16.2.0" + }, + "bin": { + "pkg-fetch": "lib-es5/bin.js" + } + }, + "node_modules/pkg-fetch/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/pkg-fetch/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC" + }, + "node_modules/pkg-fetch/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-fetch/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/pkg-fetch/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/openapi-types": { - "version": "12.1.3", - "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", - "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "node_modules/pkg-fetch/node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "dev": true, "license": "MIT", - "peer": true + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } }, - "node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "node_modules/pkg/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "aggregate-error": "^3.0.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "node_modules/pkg/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">=8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "node_modules/pkg/node_modules/is-core-module": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", + "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", + "dev": true, "license": "MIT", - "engines": { - "node": ">=0.10.0" + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "node_modules/pkg/node_modules/prebuild-install": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", + "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", "dev": true, - "license": "MIT" - }, - "node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "node_modules/pkg/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, - "license": "ISC" + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "node_modules/playwright": { + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0.tgz", + "integrity": "sha512-eKpmys0UFDnfNb3vfsf8Vx2LEOtflgRebl0Im2eQQnYMA4Aqd+Zw8bEOB+7ZKvN76901mRnqdsiOGKxzVTbi7A==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.49.0" + }, + "bin": { + "playwright": "cli.js" + }, "engines": { - "node": ">=8.6" + "node": ">=18" }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0.tgz", + "integrity": "sha512-R+3KKTQF3npy5GTiKH/T+kdhoJfJojjHESR1YEWhYuEKRVfVaxH3+4+GvXE5xyCngCxhxnykk0Vlah9v8fs3jA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" } }, "node_modules/prebuild-install": { @@ -2974,6 +4587,23 @@ "node": ">=10" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -3022,11 +4652,14 @@ "node": ">= 0.10" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } }, "node_modules/pstree.remy": { "version": "1.1.8", @@ -3036,28 +4669,20 @@ "license": "MIT" }, "node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, - "node_modules/python-shell": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/python-shell/-/python-shell-5.0.0.tgz", - "integrity": "sha512-RUOOOjHLhgR1MIQrCtnEqz/HJ1RMZBIN+REnpSUrfft2bXqXy69fwJASVziWExfFXsR1bCY0TznnHooNsCo0/w==", - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, "node_modules/qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.0.6" }, @@ -3068,10 +4693,32 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -3080,6 +4727,7 @@ "version": "2.5.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -3105,6 +4753,12 @@ "rc": "cli.js" } }, + "node_modules/rc/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -3120,16 +4774,16 @@ } }, "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, "engines": { - "node": ">=8.10.0" + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/rechoir": { @@ -3155,6 +4809,16 @@ "regexp-tree": "bin/regexp-tree" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -3183,6 +4847,46 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -3193,6 +4897,17 @@ "node": ">= 4" } }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -3200,13 +4915,37 @@ "deprecated": "Rimraf versions prior to v4 are no longer supported", "license": "ISC", "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" } }, "node_modules/safe-buffer": { @@ -3240,9 +4979,9 @@ } }, "node_modules/safe-stable-stringify": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", - "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", "license": "MIT", "engines": { "node": ">=10" @@ -3254,12 +4993,6 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, - "node_modules/scmp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/scmp/-/scmp-2.1.0.tgz", - "integrity": "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==", - "license": "BSD-3-Clause" - }, "node_modules/semver": { "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", @@ -3276,6 +5009,7 @@ "version": "0.19.0", "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -3299,6 +5033,7 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", "dependencies": { "ms": "2.0.0" } @@ -3306,25 +5041,23 @@ "node_modules/send/node_modules/debug/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" }, "node_modules/send/node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, "node_modules/serve-static": { "version": "1.16.2", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", @@ -3345,6 +5078,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -3360,12 +5094,14 @@ "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" }, "node_modules/side-channel": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "es-errors": "^1.3.0", @@ -3459,6 +5195,16 @@ "dev": true, "license": "MIT" }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -3507,10 +5253,11 @@ "license": "ISC" }, "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "license": "BSD-3-Clause" + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause", + "optional": true }, "node_modules/sqlite3": { "version": "5.1.7", @@ -3536,10 +5283,16 @@ } } }, + "node_modules/sqlite3/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, "node_modules/ssh2": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.15.0.tgz", - "integrity": "sha512-C0PHgX4h6lBxYx7hcXwu3QWdh4tg6tZZsTfXcdvc5caW/EMxaB4H9dWsl7qk+F7LAW762hp8VbXOX7x4xUYvEw==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.16.0.tgz", + "integrity": "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg==", "hasInstallScript": true, "dependencies": { "asn1": "^0.2.6", @@ -3549,8 +5302,8 @@ "node": ">=10.16.0" }, "optionalDependencies": { - "cpu-features": "~0.0.9", - "nan": "^2.18.0" + "cpu-features": "~0.0.10", + "nan": "^2.20.0" } }, "node_modules/ssri": { @@ -3579,10 +5332,67 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stream-meter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/stream-meter/-/stream-meter-1.0.4.tgz", + "integrity": "sha512-4sOEtrbgFotXwnEuzzsQBYEV1elAeFSO8rSGeTwabuX1RRn/kEq9JVH7I0MRBhKVRR0sJkr0M0QCH7yOLf9fhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.1.4" + } + }, + "node_modules/stream-meter/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/stream-meter/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/stream-meter/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -3683,6 +5493,15 @@ "node": ">=12.0.0" } }, + "node_modules/swagger-jsdoc/node_modules/commander": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", + "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/swagger-jsdoc/node_modules/glob": { "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", @@ -3717,10 +5536,13 @@ } }, "node_modules/swagger-ui-dist": { - "version": "5.17.14", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.17.14.tgz", - "integrity": "sha512-CVbSfaLpstV65OnSjbXfVd6Sta3q3F7Cj/yYuvHMp1P90LztOLs6PfUnKEVAeiIVQt9u2SaPwv0LiH/OyMjHRw==", - "license": "Apache-2.0" + "version": "5.18.2", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.18.2.tgz", + "integrity": "sha512-J+y4mCw/zXh1FOj5wGJvnAajq6XgHOyywsa9yITmwxIlJbMqITq3gYRZHaeqLVH/eV/HOPphE6NjF+nbSNC5Zw==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } }, "node_modules/swagger-ui-express": { "version": "5.0.1", @@ -3776,6 +5598,12 @@ "tar-stream": "^2.0.0" } }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, "node_modules/tar-stream": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", @@ -3792,15 +5620,6 @@ "node": ">=6" } }, - "node_modules/tar/node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, "node_modules/tar/node_modules/minipass": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", @@ -3823,6 +5642,16 @@ "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", "license": "MIT" }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3840,6 +5669,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", "engines": { "node": ">=0.6" } @@ -3869,6 +5699,50 @@ "node": ">= 14.0.0" } }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, "node_modules/tsconfig-paths": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", @@ -3885,20 +5759,96 @@ } }, "node_modules/tsconfig-paths-webpack-plugin": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.1.0.tgz", - "integrity": "sha512-xWFISjviPydmtmgeUAuXp4N1fky+VCtfhOkDUFIv5ea7p4wuTomI4QTrXvFBX2S4jZsmyTSrStQl+E+4w+RzxA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz", + "integrity": "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==", "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.1.0", "enhanced-resolve": "^5.7.0", + "tapable": "^2.2.1", "tsconfig-paths": "^4.1.2" }, "engines": { "node": ">=10.13.0" } }, + "node_modules/tsconfig-paths-webpack-plugin/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/tsconfig-paths-webpack-plugin/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/tsconfig-paths-webpack-plugin/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tsx": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz", + "integrity": "sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.23.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -3917,28 +5867,11 @@ "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", "license": "Unlicense" }, - "node_modules/twilio": { - "version": "5.3.5", - "resolved": "https://registry.npmjs.org/twilio/-/twilio-5.3.5.tgz", - "integrity": "sha512-f/sA1Yd6TyIzfcq0u4QDGU+93afwswsJB+rf3T08tvBAMobBDVR3DfGREwJr5jp8xUic0qWa7GbJidk16NA4bg==", - "license": "MIT", - "dependencies": { - "axios": "^1.7.4", - "dayjs": "^1.11.9", - "https-proxy-agent": "^5.0.0", - "jsonwebtoken": "^9.0.2", - "qs": "^6.9.4", - "scmp": "^2.1.0", - "xmlbuilder": "^13.0.2" - }, - "engines": { - "node": ">=14.0" - } - }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" @@ -3947,6 +5880,34 @@ "node": ">= 0.6" } }, + "node_modules/typescript": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -3954,6 +5915,12 @@ "dev": true, "license": "MIT" }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "license": "MIT" + }, "node_modules/unique-filename": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", @@ -3974,10 +5941,21 @@ "imurmurhash": "^0.1.4" } }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -3997,6 +5975,13 @@ "node": ">= 0.4.0" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, "node_modules/validator": { "version": "13.12.0", "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", @@ -4016,9 +6001,9 @@ } }, "node_modules/watskeburt": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/watskeburt/-/watskeburt-4.1.0.tgz", - "integrity": "sha512-KkY5H51ajqy9HYYI+u9SIURcWnqeVVhdH0I+ab6aXPGHfZYxgRCwnR6Lm3+TYB6jJVt5jFqw4GAKmwf1zHmGQw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/watskeburt/-/watskeburt-4.2.2.tgz", + "integrity": "sha512-AOCg1UYxWpiHW1tUwqpJau8vzarZYTtzl2uu99UptBmbzx6kOzCGMfRLF6KIRX4PYekmryn89MzxlRNkL66YyA==", "dev": true, "license": "MIT", "bin": { @@ -4079,34 +6064,34 @@ } }, "node_modules/winston": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.15.0.tgz", - "integrity": "sha512-RhruH2Cj0bV0WgNL+lOfoUBI4DVfdUNjVnJGVovWZmrcKtrFTTRzgXYK2O9cymSGjrERCtaAeHwMNnUWXlwZow==", + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", "license": "MIT", "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", - "logform": "^2.6.0", + "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", - "winston-transport": "^4.7.0" + "winston-transport": "^4.9.0" }, "engines": { "node": ">= 12.0.0" } }, "node_modules/winston-transport": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.7.1.tgz", - "integrity": "sha512-wQCXXVgfv/wUPOfb2x0ruxzwkcZfxcktz6JIMUaPLmcNhO4bZTwA/WtDWK74xV3F2dKu8YadrFv0qhwYjVEwhA==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", "license": "MIT", "dependencies": { - "logform": "^2.6.1", + "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" }, @@ -4114,19 +6099,38 @@ "node": ">= 12.0.0" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, - "node_modules/xmlbuilder": { - "version": "13.0.2", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-13.0.2.tgz", - "integrity": "sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==", - "license": "MIT", + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", "engines": { - "node": ">=6.0" + "node": ">=10" } }, "node_modules/yallist": { @@ -4144,18 +6148,43 @@ "node": ">= 6" } }, - "node_modules/yamljs": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz", - "integrity": "sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==", + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, "license": "MIT", "dependencies": { - "argparse": "^1.0.7", - "glob": "^7.0.5" + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" }, - "bin": { - "json2yaml": "bin/json2yaml", - "yaml2json": "bin/yaml2json" + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" } }, "node_modules/z-schema": { diff --git a/package.json b/package.json index b1c3b7c..78ac946 100644 --- a/package.json +++ b/package.json @@ -2,43 +2,68 @@ "name": "dockstatapi", "version": "2", "description": "API for docker hosts using dockerode", - "main": "server.js", + "main": "src/server.ts", "scripts": { - "start": "node server.js", - "dev": "nodemon server.js", - "offline": "OFFLINE=true nodemon server.js", - "dep": "bash ./utils/createDependencyGraph.sh" + "start": "tsx src/server.ts", + "start:build": "npx tsc && node dist/server.js", + "dev": "nodemon", + "dev:trace": "nodemon --trace-uncaught --trace-warnings", + "dep": "bash ./src/utils/createDependencyGraph.sh", + "dep:remove": "bash ./src/utils/removeUnusedDeps.sh && bash ./src/utils/createDependencyGraph.sh", + "build": "npx tsc", + "build:mini": "npx tsc && bash ./src/misc/minifyDist.sh --build-only", + "mini": "bash ./src/misc/minifyDist.sh" }, "keywords": [], "author": "Its4Nik", "license": "BSD 3-Clause License", "dependencies": { + "@types/dockerode": "^3.3.31", + "@types/supports-color": "^8.1.3", + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.7", "bcrypt": "^5.1.1", - "child_process": "^1.0.2", + "chokidar": "^4.0.1", "cors": "^2.8.5", "dockerode": "^4.0.2", "express": "^4.21.1", "express-rate-limit": "^7.4.1", - "js-yaml": "^4.1.0", + "https": "^1.0.0", + "ipaddr.js": "^2.2.0", "node-fetch": "^3.3.2", "nodemailer": "^6.9.16", - "python-shell": "^5.0.0", "sqlite3": "^5.1.7", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1", - "twilio": "^5.3.5", - "winston": "^3.15.0", - "yamljs": "^0.3.0" + "winston": "^3.15.0" }, "devDependencies": { + "@playwright/test": "^1.49.0", + "@types/bcrypt": "^5.0.2", + "@types/cors": "^2.8.17", + "@types/express": "^5.0.0", + "@types/express-handlebars": "^5.3.1", + "@types/node": "^22.9.0", + "@types/node-fetch": "^2.6.12", + "@types/nodemailer": "^6.4.17", "dependency-cruiser": "^16.5.0", - "nodemon": "^3.1.7" + "nodemon": "^3.1.7", + "ora": "^8.1.1", + "pkg": "^5.8.1", + "ts-node": "^10.9.2", + "tsx": "^4.19.2", + "uglify-js": "^3.19.3" }, "nodemonConfig": { "ignore": [ - "**/logs/**", - "**/data/**" + "**/data/**", + "**/*.json", + ".gitignore" ], "delay": 2500 - } + }, + "engines": { + "npm": ">=10.8.2" + }, + "repository": "git@github.com:Its4Nik/dockstatapi.git" } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..2c33a93 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,37 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + timeout: 300000, + testDir: './tests', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: 'html', + use: { + trace: 'on-first-retry', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + ], + + webServer: { + command: 'npm run start', + url: 'http://127.0.0.1:9876', + reuseExistingServer: true + }, +}); diff --git a/routes/auth/routes.js b/routes/auth/routes.js deleted file mode 100644 index bbc20d4..0000000 --- a/routes/auth/routes.js +++ /dev/null @@ -1,146 +0,0 @@ -const express = require("express"); -const bcrypt = require("bcrypt"); -const fs = require("fs"); -const path = require("path"); -const logger = require("../../utils/logger"); -const router = express.Router(); -const passwordFile = path.join(__dirname, "../../middleware/password.json"); -const passwordBool = path.join(__dirname, "../../middleware/usePassword.txt"); -const saltRounds = 10; - -function setTrue() { - fs.writeFile(passwordBool, "true", "utf8", (err) => { - if (err) { - logger.error("Error writing to the file:", err); - return; - } - logger.info(`Status "true" has been written to the file.`); - }); -} - -function setFalse() { - fs.writeFile(passwordBool, "false", "utf8", (err) => { - if (err) { - logger.error("Error writing to the file:", err); - return; - } - logger.info(`Status "false" has been written to the file.`); - }); -} - -/** - * @swagger - * /auth/enable: - * post: - * summary: Enable authentication by setting a password - * tags: [Authentication] - * parameters: - * - name: password - * in: query - * required: true - * responses: - * 200: - * description: Authentication enabled. - * 400: - * description: Password is required. - * 500: - * description: Error saving password. - */ -router.post("/enable", (req, res) => { - fs.readFile(passwordBool, "utf8", (err, data) => { - const password = req.query.password; - if (err) { - logger.error("Error reading the file:", err); - return; - } - - const isAuthEnabled = data.trim() === "true"; - if (isAuthEnabled) { - logger.error( - "Passowrd Authentication is already enabled, please dactivate it first", - ); - return res.status(401).json({ - message: - "Passowrd Authentication is already enabled, please dactivate it first", - }); - } - - if (!password) { - return res.status(400).json({ message: "Password is required" }); - } - - bcrypt.genSalt(saltRounds, (err, salt) => { - if (err) { - logger.error("Error generating salt"); - return res.status(500).json({ message: "Error generating salt" }); - } - - bcrypt.hash(password, salt, (err, hash) => { - if (err) { - logger.error("Error hashing password"); - return res.status(500).json({ message: "Error hashing password" }); - } - - const passwordData = { hash, salt }; - fs.writeFile(passwordFile, JSON.stringify(passwordData), (err) => { - if (err) { - return res.status(500).json({ message: "Error saving password" }); - } - setTrue(); - res.json({ message: "Authentication enabled" }); - }); - }); - }); - }); -}); - -/** - * @swagger - * /auth/disable: - * post: - * summary: Disable authentication by providing the existing password - * tags: [Authentication] - * parameters: - * - name: password - * in: query - * required: true - * responses: - * 200: - * description: Authentication disabled. - * 400: - * description: Password is required. - * 401: - * description: Invalid password. - * 500: - * description: Error disabling authentication. - */ -router.post("/disable", (req, res) => { - const password = req.query.password; - if (!password) { - logger.error("Password is required!"); - return res.status(400).json({ message: "Password is required" }); - } - - fs.readFile(passwordFile, "utf8", (err, data) => { - if (err) { - logger.error("Error reading password"); - return res.status(500).json({ message: "Error reading password" }); - } - - const storedData = JSON.parse(data); - bcrypt.compare(password, storedData.hash, (err, result) => { - if (err) { - logger.error("Error validating password"); - return res.status(500).json({ message: "Error validating password" }); - } - if (!result) { - logger.error("Invalid password"); - return res.status(401).json({ message: "Invalid password" }); - } - setFalse(); - res.json({ message: "Authentication disabled" }); - }); - }); -}); - -module.exports = router; diff --git a/routes/data/routes.js b/routes/data/routes.js deleted file mode 100644 index adce8d7..0000000 --- a/routes/data/routes.js +++ /dev/null @@ -1,111 +0,0 @@ -const express = require("express"); -const router = express.Router(); -const db = require("../../config/db"); -const logger = require("../../utils/logger"); - -function formatRows(rows) { - return rows.reduce((acc, row, index) => { - acc[index] = JSON.parse(row.info); - return acc; - }, {}); -} - -/** - * @swagger - * /data/latest: - * get: - * summary: Retrieve the latest entry from the database - * tags: [Database queries] - * responses: - * 200: - * description: A JSON object containing the latest entry's 'info' data. - * content: - * application/json: - * schema: - * type: object - * example: - * name: "Container A" - * id: "abcd1234" - * cpu_usage: 30 - * mem_usage: 2048 - */ -router.get("/latest", (req, res) => { - db.get( - "SELECT info FROM data ORDER BY timestamp DESC LIMIT 1", - (err, row) => { - if (err) { - logger.error("Error fetching latest data:", err.message); - return res.status(500).json({ error: "Internal server error" }); - } - res.json(JSON.parse(row.info)); - }, - ); -}); - -/** - * @swagger - * /data/time/24h: - * get: - * summary: Retrieve entries from the last 24 hours from the database - * tags: [Database queries] - * responses: - * 200: - * description: A numbered array of 'info' JSON objects from the last 24 hours. - * content: - * application/json: - * schema: - * type: object - * example: - * 0: - * name: "Container A" - * id: "abcd1234" - * cpu_usage: 30 - * mem_usage: 2048 - * 1: - * name: "Container B" - * id: "efgh5678" - * cpu_usage: 45 - * mem_usage: 3072 - */ -router.get("/time/24h", (req, res) => { - const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); - db.all( - "SELECT info FROM data WHERE timestamp >= ?", - [oneDayAgo], - (err, rows) => { - if (err) { - logger.error("Error fetching data from last 24 hours:", err.message); - return res.status(500).json({ error: "Internal server error" }); - } - res.json(formatRows(rows)); - }, - ); -}); - -/** - * @swagger - * /data/clear: - * delete: - * summary: Clear all entries from the database - * tags: [Database queries] - * responses: - * 200: - * description: A message indicating whether the database was cleared successfully. - * content: - * application/json: - * schema: - * type: object - * example: - * message: "Database cleared successfully." - */ -router.delete("/clear", (req, res) => { - db.run("DELETE FROM data", (err) => { - if (err) { - logger.error("Error clearing the database:", err.message); - return res.status(500).json({ error: "Internal server error" }); - } - res.json({ message: "Database cleared successfully" }); - }); -}); - -module.exports = router; diff --git a/routes/setter/routes.js b/routes/setter/routes.js deleted file mode 100644 index 24ae2ad..0000000 --- a/routes/setter/routes.js +++ /dev/null @@ -1,145 +0,0 @@ -const { - setFetchInterval, - parseInterval, -} = require("../../controllers/scheduler"); -const express = require("express"); -const router = express.Router(); -const path = require("path"); -const fs = require("fs"); -const logger = require("../../utils/logger"); - -/** - * @swagger - * /conf/addHost: - * put: - * summary: Add a new host to the Docker configuration - * tags: [Configuration] - * parameters: - * - name: name - * in: query - * required: true - * description: The name of the new host. - * - name: url - * in: query - * required: true - * description: The URL of the new host. - * - name: port - * in: query - * required: true - * description: The port of the new host. - * responses: - * 200: - * description: Host added successfully. - * 400: - * description: Bad request, invalid input. - * 500: - * description: An error occurred while adding the host. - */ -router.put("/addHost", async (req, res) => { - const name = req.query.name; - const url = req.query.url; - const port = req.query.port; - const configPath = path.join(__dirname, "../../config/dockerConfig.json"); - - if (!name || !url || !port) { - return res.status(400).json({ error: "Name, Port and URL are required." }); - } - - try { - const rawData = fs.readFileSync(configPath); - const config = JSON.parse(rawData); - - // Check for existing host - if (config.hosts.some((host) => host.name === name)) { - return res.status(400).json({ error: "Host already exists." }); - } - - config.hosts.push({ name, url, port }); - fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); - logger.info(`Added new host: ${name}`); - res.status(200).json({ message: "Host added successfully." }); - } catch (error) { - logger.error("Error adding host: " + error.message); - res.status(500).json({ error: "Failed to add host." }); - } -}); - -/** - * @swagger - * /conf/scheduler: - * put: - * summary: Set fetch interval for data fetching - * tags: [Configuration] - * parameters: - * - name: interval - * in: query - * required: true - * description: The new interval for fetching data, e.g., "6h 20m", "300s". - * responses: - * 200: - * description: Fetch interval set successfully. - * 400: - * description: Invalid interval format or out of range. - */ -router.put("/scheduler", (req, res) => { - const interval = req.query.interval; - const newInterval = parseInterval(interval); - - if (newInterval < 5 * 60 * 1000 || newInterval > 6 * 60 * 60 * 1000) { - return res - .status(400) - .json({ error: "Interval must be between 5 minutes and 6 hours." }); - } - - setFetchInterval(newInterval); - res.json({ message: `Fetch interval set to ${interval}.` }); -}); - -/** - * @swagger - * /conf/removeHost: - * delete: - * summary: Remove a host from the Docker configuration - * tags: [Configuration] - * parameters: - * - name: hostName - * in: query - * required: true - * description: The name of the host to remove. - * responses: - * 200: - * description: Host removed successfully. - * 404: - * description: Host not found. - * 500: - * description: An error occurred while removing the host. - */ -router.delete("/removeHost", async (req, res) => { - const hostName = req.query.hostName; - const configPath = path.join(__dirname, "../../config/dockerConfig.json"); - - if (!hostName) { - return res.status(400).json({ error: "Host name is required." }); - } - - try { - const rawData = fs.readFileSync(configPath); - const config = JSON.parse(rawData); - - // Check for existing host - const hostIndex = config.hosts.findIndex((host) => host.name === hostName); - if (hostIndex === -1) { - return res.status(404).json({ error: "Host not found." }); - } - - config.hosts.splice(hostIndex, 1); - fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); - logger.info(`Removed host: ${hostName}`); - res.status(200).json({ message: "Host removed successfully." }); - } catch (error) { - logger.error("Error removing host: " + error.message); - res.status(500).json({ error: "Failed to remove host." }); - } -}); - -module.exports = router; diff --git a/server.js b/server.js deleted file mode 100644 index d383cde..0000000 --- a/server.js +++ /dev/null @@ -1,49 +0,0 @@ -const express = require("express"); -const router = express.Router(); -const app = express(); - -// Utility: -const swaggerDocs = require("./swagger/swaggerDocs"); -const logger = require("./utils/logger"); - -// Routes: -const api = require("./routes/getter/routes"); -const conf = require("./routes/setter/routes"); -const auth = require("./routes/auth/routes"); -const data = require("./routes/data/routes"); -const frontend = require("./routes/frontendController/routes"); -const notificationService = require("./routes/notifications/routes"); - -// Middleware: -const authMiddleware = require("./middleware/authMiddleware"); -const { limiter } = require("./middleware/rateLimiter"); - -// Controllers -const { scheduleFetch } = require("./controllers/scheduler"); - -const PORT = "7070"; - -app.use(express.json()); - -app.use("/api-docs", (req, res, next) => next()); - -swaggerDocs(app); -scheduleFetch(); - -// Routes -app.use("/api", limiter, authMiddleware, api); -app.use("/conf", limiter, authMiddleware, conf); -app.use("/auth", limiter, authMiddleware, auth); -app.use("/data", limiter, authMiddleware, data); -app.use("/frontend", limiter, authMiddleware, frontend); -app.use("/notification-service", limiter, authMiddleware, notificationService); - -// Default route -router.get("/", (req, res) => { - res.redirect("/api-docs"); -}); - -app.listen(PORT, () => { - logger.info(`Server is running on http://localhost:${PORT}`); - logger.info(`Swagger docs available at http://localhost:${PORT}/api-docs`); -}); diff --git a/.dependency-cruiser.js b/src/.dependency-cruiser.cjs similarity index 66% rename from .dependency-cruiser.js rename to src/.dependency-cruiser.cjs index 07df12b..d734a68 100644 --- a/.dependency-cruiser.js +++ b/src/.dependency-cruiser.cjs @@ -2,87 +2,83 @@ module.exports = { forbidden: [ { - name: 'no-circular', - severity: 'warn', + name: "no-circular", + severity: "warn", comment: - 'This dependency is part of a circular relationship. You might want to revise ' + - 'your solution (i.e. use dependency inversion, make sure the modules have a single responsibility) ', + "This dependency is part of a circular relationship. You might want to revise " + + "your solution (i.e. use dependency inversion, make sure the modules have a single responsibility) ", from: {}, to: { - circular: true - } + circular: true, + }, }, { - name: 'no-orphans', + name: "no-orphans", comment: "This is an orphan module - it's likely not used (anymore?). Either use it or " + "remove it. If it's logical this module is an orphan (i.e. it's a config file), " + "add an exception for it in your dependency-cruiser configuration. By default " + "this rule does not scrutinize dot-files (e.g. .eslintrc.js), TypeScript declaration " + "files (.d.ts), tsconfig.json and some of the babel and webpack configs.", - severity: 'warn', + severity: "warn", from: { orphan: true, pathNot: [ - '(^|/)[.][^/]+[.](?:js|cjs|mjs|ts|cts|mts|json)$', // dot files - '[.]d[.]ts$', // TypeScript declaration files - '(^|/)tsconfig[.]json$', // TypeScript config - '(^|/)(?:babel|webpack)[.]config[.](?:js|cjs|mjs|ts|cts|mts|json)$' // other configs - ] + "(^|/)[.][^/]+[.](?:js|cjs|mjs|ts|cts|mts|json)$", // dot files + "[.]d[.]ts$", // TypeScript declaration files + "(^|/)tsconfig[.]json$", // TypeScript config + "(^|/)(?:babel|webpack)[.]config[.](?:js|cjs|mjs|ts|cts|mts|json)$", // other configs + ], }, to: {}, }, { - name: 'no-deprecated-core', + name: "no-deprecated-core", comment: - 'A module depends on a node core module that has been deprecated. Find an alternative - these are ' + + "A module depends on a node core module that has been deprecated. Find an alternative - these are " + "bound to exist - node doesn't deprecate lightly.", - severity: 'warn', + severity: "warn", from: {}, to: { - dependencyTypes: [ - 'core' - ], + dependencyTypes: ["core"], path: [ - '^v8/tools/codemap$', - '^v8/tools/consarray$', - '^v8/tools/csvparser$', - '^v8/tools/logreader$', - '^v8/tools/profile_view$', - '^v8/tools/profile$', - '^v8/tools/SourceMap$', - '^v8/tools/splaytree$', - '^v8/tools/tickprocessor-driver$', - '^v8/tools/tickprocessor$', - '^node-inspect/lib/_inspect$', - '^node-inspect/lib/internal/inspect_client$', - '^node-inspect/lib/internal/inspect_repl$', - '^async_hooks$', - '^punycode$', - '^domain$', - '^constants$', - '^sys$', - '^_linklist$', - '^_stream_wrap$' + "^v8/tools/codemap$", + "^v8/tools/consarray$", + "^v8/tools/csvparser$", + "^v8/tools/logreader$", + "^v8/tools/profile_view$", + "^v8/tools/profile$", + "^v8/tools/SourceMap$", + "^v8/tools/splaytree$", + "^v8/tools/tickprocessor-driver$", + "^v8/tools/tickprocessor$", + "^node-inspect/lib/_inspect$", + "^node-inspect/lib/internal/inspect_client$", + "^node-inspect/lib/internal/inspect_repl$", + "^async_hooks$", + "^punycode$", + "^domain$", + "^constants$", + "^sys$", + "^_linklist$", + "^_stream_wrap$", ], - } + }, }, { - name: 'not-to-deprecated', + name: "not-to-deprecated", comment: - 'This module uses a (version of an) npm module that has been deprecated. Either upgrade to a later ' + - 'version of that module, or find an alternative. Deprecated modules are a security risk.', - severity: 'warn', + "This module uses a (version of an) npm module that has been deprecated. Either upgrade to a later " + + "version of that module, or find an alternative. Deprecated modules are a security risk.", + severity: "warn", from: {}, to: { - dependencyTypes: [ - 'deprecated' - ] - } + dependencyTypes: ["deprecated"], + }, }, { - name: 'no-non-package-json', - severity: 'error', + name: "no-non-package-json", + severity: "error", comment: "This module depends on an npm package that isn't in the 'dependencies' section of your package.json. " + "That's problematic as the package either (1) won't be available on live (2 - worse) will be " + @@ -90,84 +86,75 @@ module.exports = { "in your package.json.", from: {}, to: { - dependencyTypes: [ - 'npm-no-pkg', - 'npm-unknown' - ] - } + dependencyTypes: ["npm-no-pkg", "npm-unknown"], + }, }, { - name: 'not-to-unresolvable', + name: "not-to-unresolvable", comment: "This module depends on a module that cannot be found ('resolved to disk'). If it's an npm " + - 'module: add it to your package.json. In all other cases you likely already know what to do.', - severity: 'error', + "module: add it to your package.json. In all other cases you likely already know what to do.", + severity: "error", from: {}, to: { - couldNotResolve: true - } + couldNotResolve: true, + }, }, { - name: 'no-duplicate-dep-types', + name: "no-duplicate-dep-types", comment: "Likely this module depends on an external ('npm') package that occurs more than once " + "in your package.json i.e. bot as a devDependencies and in dependencies. This will cause " + "maintenance problems later on.", - severity: 'warn', + severity: "warn", from: {}, to: { moreThanOneDependencyType: true, - // as it's pretty common to have a type import be a type only import + // as it's pretty common to have a type import be a type only import // _and_ (e.g.) a devDependency - don't consider type-only dependency // types for this rule - dependencyTypesNot: ["type-only"] - } + dependencyTypesNot: ["type-only"], + }, }, /* rules you might want to tweak for your specific situation: */ - + { - name: 'not-to-spec', + name: "not-to-spec", comment: - 'This module depends on a spec (test) file. The sole responsibility of a spec file is to test code. ' + + "This module depends on a spec (test) file. The sole responsibility of a spec file is to test code. " + "If there's something in a spec that's of use to other modules, it doesn't have that single " + - 'responsibility anymore. Factor it out into (e.g.) a separate utility/ helper or a mock.', - severity: 'error', + "responsibility anymore. Factor it out into (e.g.) a separate utility/ helper or a mock.", + severity: "error", from: {}, to: { - path: '[.](?:spec|test)[.](?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$' - } + path: "[.](?:spec|test)[.](?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$", + }, }, { - name: 'not-to-dev-dep', - severity: 'error', + name: "not-to-dev-dep", + severity: "error", comment: "This module depends on an npm package from the 'devDependencies' section of your " + - 'package.json. It looks like something that ships to production, though. To prevent problems ' + + "package.json. It looks like something that ships to production, though. To prevent problems " + "with npm packages that aren't there on production declare it (only!) in the 'dependencies'" + - 'section of your package.json. If this module is development only - add it to the ' + - 'from.pathNot re of the not-to-dev-dep rule in the dependency-cruiser configuration', + "section of your package.json. If this module is development only - add it to the " + + "from.pathNot re of the not-to-dev-dep rule in the dependency-cruiser configuration", from: { - path: '^(\./)', - pathNot: '[.](?:spec|test)[.](?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$' + path: "^(./)", + pathNot: "[.](?:spec|test)[.](?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$", }, to: { - dependencyTypes: [ - 'npm-dev', - ], + dependencyTypes: ["npm-dev"], // type only dependencies are not a problem as they don't end up in the // production code or are ignored by the runtime. - dependencyTypesNot: [ - 'type-only' - ], - pathNot: [ - 'node_modules/@types/' - ] - } + dependencyTypesNot: ["type-only"], + pathNot: ["node_modules/@types/"], + }, }, { - name: 'optional-deps-used', - severity: 'info', + name: "optional-deps-used", + severity: "info", comment: "This module depends on an npm package that is declared as an optional dependency " + "in your package.json. As this makes sense in limited situations only, it's flagged here. " + @@ -175,33 +162,28 @@ module.exports = { "dependency-cruiser configuration.", from: {}, to: { - dependencyTypes: [ - 'npm-optional' - ] - } + dependencyTypes: ["npm-optional"], + }, }, { - name: 'peer-deps-used', + name: "peer-deps-used", comment: "This module depends on an npm package that is declared as a peer dependency " + "in your package.json. This makes sense if your package is e.g. a plugin, but in " + "other cases - maybe not so much. If the use of a peer dependency is intentional " + "add an exception to your dependency-cruiser configuration.", - severity: 'warn', + severity: "warn", from: {}, to: { - dependencyTypes: [ - 'npm-peer' - ] - } - } + dependencyTypes: ["npm-peer"], + }, + }, ], options: { - /* Which modules not to follow further when encountered */ doNotFollow: { /* path: an array of regular expressions in strings to match against */ - path: ['node_modules'] + path: ["../node_modules"], }, /* Which modules to exclude */ @@ -220,15 +202,15 @@ module.exports = { module systems it knows of. It's the default because it's the safe option It might come at a performance penalty, though. moduleSystems: ['amd', 'cjs', 'es6', 'tsd'] - + As in practice only commonjs ('cjs') and ecmascript modules ('es6') are widely used, you can limit the moduleSystems to those. */ - + // moduleSystems: ['cjs', 'es6'], /* prefix for links in html and svg output (e.g. 'https://github.com/you/yourrepo/blob/main/' - to open it on your online repo or `vscode://file/${process.cwd()}/` to + to open it on your online repo or `vscode://file/${process.cwd()}/` to open it in visual studio code), */ // prefix: `vscode://file/${process.cwd()}/`, @@ -238,7 +220,7 @@ module.exports = { "specify": for each dependency identify whether it only exists before compilation or also after */ // tsPreCompilationDeps: false, - + /* list of extensions to scan that aren't javascript or compile-to-javascript. Empty by default. Only put extensions in here that you want to take into account that are _not_ parsable. @@ -262,9 +244,9 @@ module.exports = { dependency-cruiser's current working directory). When not provided defaults to './tsconfig.json'. */ - // tsConfig: { - // fileName: 'tsconfig.json' - // }, + //tsConfig: { + //fileName: "../tsconfig.json", + //}, /* Webpack configuration to use to get resolve options from. @@ -273,7 +255,7 @@ module.exports = { to './webpack.conf.js'. The (optional) `env` and `arguments` attributes contain the parameters - to be passed if your webpack config is a function and takes them (see + to be passed if your webpack config is a function and takes them (see webpack documentation for details) */ // webpackConfig: { @@ -295,7 +277,7 @@ module.exports = { a hack. */ // exoticRequireStrings: [], - + /* options to pass on to enhanced-resolve, the package dependency-cruiser uses to resolve module references to disk. The values below should be suitable for most situations @@ -304,7 +286,7 @@ module.exports = { there will override the ones specified here. */ enhancedResolveOptions: { - /* What to consider as an 'exports' field in package.jsons */ + /* What to consider as an 'exports' field in package.jsons */ exportsFields: ["exports"], /* List of conditions to check for in the exports field. Only works when the 'exportsFields' array is non-empty. @@ -314,22 +296,18 @@ module.exports = { The extensions, by default are the same as the ones dependency-cruiser can access (run `npx depcruise --info` to see which ones that are in _your_ environment). If that list is larger than you need you can pass - the extensions you actually use (e.g. [".js", ".jsx"]). This can speed + the extensions you actually use (e.g. ["", ".jsx"]). This can speed up module resolution, which is the most expensive step. */ - // extensions: [".js", ".jsx", ".ts", ".tsx", ".d.ts"], + extensions: ["", ".jsx", ".ts", ".tsx"], /* What to consider a 'main' field in package.json */ - - // if you migrate to ESM (or are in an ESM environment already) you will want to - // have "module" in the list of mainFields, like so: - // mainFields: ["module", "main", "types", "typings"], - mainFields: ["main", "types", "typings"], + mainFields: ["module", "main", "types", "typings"], /* A list of alias fields in package.jsons See [this specification](https://github.com/defunctzombie/package-browser-field-spec) and the webpack [resolve.alias](https://webpack.js.org/configuration/resolve/#resolvealiasfields) - documentation - + documentation + Defaults to an empty array (= don't use alias fields). */ // aliasFields: ["browser"], @@ -341,21 +319,21 @@ module.exports = { collapses everything in node_modules to one folder deep so you see the external modules, but their innards. */ - collapsePattern: 'node_modules/(?:@[^/]+/[^/]+|[^/]+)', + collapsePattern: "node_modules/(?:@[^/]+/[^/]+|[^/]+)", /* Options to tweak the appearance of your graph.See https://github.com/sverweij/dependency-cruiser/blob/main/doc/options-reference.md#reporteroptions for details and some examples. If you don't specify a theme dependency-cruiser falls back to a built-in one. */ - // theme: { - // graph: { - // /* splines: "ortho" gives straight lines, but is slow on big graphs - // splines: "true" gives bezier curves (fast, not as nice as ortho) - // */ - // splines: "true" - // }, - // } + theme: { + graph: { + /* splines: "ortho" gives straight lines, but is slow on big graphs + splines: "true" gives bezier curves (fast, not as nice as ortho) + */ + ortho: "true", + }, + }, }, archi: { /* pattern of modules that can be consolidated in the high level @@ -363,7 +341,8 @@ module.exports = { dependency graph reporter (`archi`) you probably want to tweak this collapsePattern to your situation. */ - collapsePattern: '^(?:packages|src|lib(s?)|app(s?)|bin|test(s?)|spec(s?))/[^/]+|node_modules/(?:@[^/]+/[^/]+|[^/]+)', + collapsePattern: + "^(?:packages|src|lib(s?)|app(s?)|bin|test(s?)|spec(s?))/[^/]+|node_modules/(?:@[^/]+/[^/]+|[^/]+)", /* Options to tweak the appearance of your graph. If you don't specify a theme for 'archi' dependency-cruiser will use the one specified in the @@ -371,10 +350,10 @@ module.exports = { */ // theme: { }, }, - "text": { - "highlightFocused": true + text: { + highlightFocused: true, }, - } - } + }, + }, }; -// generated: dependency-cruiser@16.5.0 on 2024-10-31T20:09:59.974Z +// generated: dependency-cruiser@16.5.0 on 2024-11-08T20:57:37.261Z diff --git a/src/config/db.ts b/src/config/db.ts new file mode 100644 index 0000000..9397213 --- /dev/null +++ b/src/config/db.ts @@ -0,0 +1,30 @@ +import sqlite3 from "sqlite3"; +import logger from "../utils/logger"; + +const dbPath: string = "./src/data/database.db"; + +const db: sqlite3.Database = new sqlite3.Database( + dbPath, + (err: Error | null) => { + if (err) { + logger.error("Error opening database:", err.message); + } else { + db.run( + `CREATE TABLE IF NOT EXISTS data ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + info TEXT NOT NULL, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP + )`, + (tableErr: Error | null) => { + if (tableErr) { + logger.error("Error creating table:", tableErr.message); + } else { + logger.info("Database created / opened successfully"); + } + }, + ); + } + }, +); + +export default db; diff --git a/src/config/hostsystem.ts b/src/config/hostsystem.ts new file mode 100644 index 0000000..520d3ef --- /dev/null +++ b/src/config/hostsystem.ts @@ -0,0 +1,61 @@ +import fs from "fs"; +import logger from "../utils/logger"; +import os from "os"; + +const userConf = "./src/data/user.conf"; +const inDocker: boolean = !!process.env.RUNNING_IN_DOCKER; +const version: string = process.env.VERSION || "unknown"; + +function writeUserConf() { + let previousConfig = null; + let shouldRewriteConfig = false; + + const installationDetails = { + installedAt: new Date().toISOString(), + backendVersion: version, + inDocker: inDocker, + installedBy: os.userInfo().username, + platform: os.platform(), + arch: os.arch(), + }; + + if (fs.existsSync(userConf)) { + try { + previousConfig = JSON.parse(fs.readFileSync(userConf, "utf-8")); + if (previousConfig.backendVersion !== version) { + shouldRewriteConfig = true; + logger.debug( + "Version change detected. Rewriting configuration file...", + ); + } else { + logger.debug("No version change detected. Skipping re-initialization."); + } + } catch (error) { + logger.error( + "Error reading the configuration file. Rewriting it...", + error, + ); + shouldRewriteConfig = true; + } + } else { + logger.debug("Configuration file not found. Creating a new one..."); + shouldRewriteConfig = true; + } + + if (shouldRewriteConfig) { + fs.writeFileSync(userConf, JSON.stringify(installationDetails, null, 2)); + logger.debug("Configuration file created/updated:", userConf); + } + + const startDetails = { + startedAt: new Date().toISOString(), + backendVersion: version, + }; + + logger.info("Starting the server..."); + logger.info( + `At: ${startDetails.startedAt} - Version: ${startDetails.backendVersion} - Docker: ${installationDetails.inDocker} - Installed as: ${installationDetails.installedBy} - Platform: ${installationDetails.platform} - Arch: ${installationDetails.arch}`, + ); +} + +export default writeUserConf; diff --git a/src/config/loggerConfig.ts b/src/config/loggerConfig.ts new file mode 100644 index 0000000..7d34f03 --- /dev/null +++ b/src/config/loggerConfig.ts @@ -0,0 +1,45 @@ +import { createLogger, format, transports } from "winston"; + +const gray = "\x1b[90m"; +const reset = "\x1b[0m"; +const white = "\x1b[97m"; +const red = "\x1b[31m"; +const green = "\x1b[32m"; +const yellow = "\x1b[33m"; +const blue = "\x1b[34m"; + +function colorLog(level: string, levelName: string) { + switch (level) { + case "info": + return `${green}${levelName}${reset}`; + case "debug": + return `${blue}${levelName}${reset}`; + case "error": + return `${red}${levelName}${reset}`; + case "warn": + return `${yellow}${levelName}${reset}`; + default: + return `${gray}UNKNOWN${reset}`; + } +} + +const logger = createLogger({ + level: "debug", + format: format.combine( + format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), + format.printf((info) => { + const level = info.level.toUpperCase().padEnd(5, " "); + const timestamp = `${gray}${info.timestamp}${reset}`; + const levelColorized = colorLog(info.level.toLowerCase(), level); + const message = `${white}${info.message}${reset}`; + + return `${timestamp} ${levelColorized} : ${message}`; + }), + ), + transports: [ + new transports.Console(), + new transports.File({ filename: "logs/app.log" }), + ], +}); + +export default logger; diff --git a/src/config/swaggerConfig.ts b/src/config/swaggerConfig.ts new file mode 100644 index 0000000..630805e --- /dev/null +++ b/src/config/swaggerConfig.ts @@ -0,0 +1,53 @@ +const options: { + definition: { + failOnErrors: boolean; + openapi: string; + info: { + title: string; + version: string; + description: string; + }; + components: { + securitySchemes: { + passwordAuth: { + type: string; + in: string; + name: string; + description: string; + }; + }; + }; + security: Array<{ + passwordAuth: any[]; + }>; + }; + apis: string[]; +} = { + definition: { + failOnErrors: true, + openapi: "3.0.0", + info: { + title: "DockStatAPI", + version: "2", + description: "An API used to query muliple docker hosts", + }, + components: { + securitySchemes: { + passwordAuth: { + type: "apiKey", + in: "header", + name: "x-password", + description: "Password required for authentication", + }, + }, + }, + security: [ + { + passwordAuth: [], + }, + ], + }, + apis: ["./src/routes/*/*.ts"], +}; + +export default options; diff --git a/controllers/containerController.js b/src/controllers/containerController.ts similarity index 58% rename from controllers/containerController.js rename to src/controllers/containerController.ts index f62ec5c..1532681 100644 --- a/controllers/containerController.js +++ b/src/controllers/containerController.ts @@ -1,27 +1,30 @@ -const fs = require("fs"); -const path = require("path"); -const { getDockerClient } = require("../utils/dockerClient"); -const logger = require("../utils/logger"); +import getDockerClient from "../utils/dockerClient"; +import logger from "../utils/logger"; +import { Request, Response } from "express"; -const getContainers = async (req, res) => { - const host = req.query.host || "local"; +const getContainers = async (req: Request, res: Response): Promise => { + const host: string = (req.query.host as string) || "local"; logger.info(`Fetching containers from host: ${host}`); try { const docker = getDockerClient(host); const containers = await docker.listContainers(); res.status(200).json(containers); - } catch (err) { + } catch (error: any) { logger.error( - `Error fetching containers from host: ${host} - ${err.message || "Unknown error"} - Full error: ${JSON.stringify(err, null, 2)}`, + `Error fetching containers from host: ${host} - ${error.message || "Unknown error"} - Full error: ${JSON.stringify(error, null, 2)}`, ); res.status(500).json({ - error: `Error fetching containers: ${err.message || "Unknown error"}`, + error: `Error fetching containers: ${error.message || "Unknown error"}`, }); } }; -const getContainerStats = async (containerID, containerHost) => { +const getContainerStats = async ( + containerID: string, + containerHost: string, + res: Response, +): Promise => { logger.info( `Fetching stats for container: ${containerID} from host: ${containerHost}`, ); @@ -33,7 +36,7 @@ const getContainerStats = async (containerID, containerHost) => { `Successfully fetched stats for container: ${containerID} from host: ${containerHost}`, ); res.status(200).json(stats); - } catch (error) { + } catch (error: any) { logger.error( `Error fetching stats for container: ${containerID} from host: ${containerHost} - ${error.message}`, ); @@ -43,7 +46,7 @@ const getContainerStats = async (containerID, containerHost) => { } }; -module.exports = { +export default { getContainers, getContainerStats, }; diff --git a/controllers/databaseMigration.js b/src/controllers/databaseMigration.ts similarity index 55% rename from controllers/databaseMigration.js rename to src/controllers/databaseMigration.ts index 263de07..45f88d1 100644 --- a/controllers/databaseMigration.js +++ b/src/controllers/databaseMigration.ts @@ -1,13 +1,13 @@ -const db = require("../config/db"); -const logger = require("../utils/logger"); +import db from "../config/db"; +import logger from "../utils/logger"; -function clearOldEntries() { - const twentyFourHoursAgo = Date.now() - 24 * 60 * 60 * 1000; +function clearOldEntries(): void { + const twentyFourHoursAgo: number = Date.now() - 24 * 60 * 60 * 1000; db.run( `DELETE FROM data WHERE createdAt < ?`, [twentyFourHoursAgo], - (err) => { + (err: Error | null) => { if (err) { logger.error("Error deleting old entries:", err.message); throw new Error("Database cleanup failed"); @@ -17,4 +17,4 @@ function clearOldEntries() { ); } -module.exports = clearOldEntries; +export default clearOldEntries; diff --git a/src/controllers/fetchData.ts b/src/controllers/fetchData.ts new file mode 100644 index 0000000..be9fdc7 --- /dev/null +++ b/src/controllers/fetchData.ts @@ -0,0 +1,80 @@ +import db from "../config/db"; +import fetchAllContainers from "../utils/containerService"; +import logger from "../utils/logger"; +import fs from "fs"; +const filePath = "./src/data/states.json"; + +let previousState: { [key: string]: string } = {}; + +interface Container { + name: string; + id: string; + state: string; + hostName: string; +} + +interface AllContainerData { + [host: string]: Container[] | { error: string }; +} + +const fetchData = async (): Promise => { + try { + const allContainerData: AllContainerData = + (await fetchAllContainers()) || {}; + + if (process.env.OFFLINE === "true") { + logger.info("No new data inserted --- OFFLINE MODE"); + } else { + db.run( + `INSERT INTO data (info) VALUES (?)`, + [JSON.stringify(allContainerData)], + function (error) { + if (error) { + logger.error("Error inserting data:", error); + return; + } + logger.info(`Data inserted with ID: ${this.lastID}`); + }, + ); + } + + const containerStatus: AllContainerData = {}; + + Object.keys(allContainerData).forEach((host) => { + const containers = allContainerData[host]; + + // Handle if the containers are an array, otherwise handle the error + if (Array.isArray(containers)) { + containerStatus[host] = containers.map((container: Container) => ({ + name: container.name, + id: container.id, + state: container.state, + hostName: container.hostName, + })); + } else { + // If there's an error, handle it separately + containerStatus[host] = { error: "Error fetching containers" }; + } + }); + + if (fs.existsSync(filePath)) { + const fileData = fs.readFileSync(filePath, "utf8"); + previousState = fileData ? JSON.parse(fileData) : {}; + } + + // Compare previous and current state + if (JSON.stringify(previousState) !== JSON.stringify(containerStatus)) { + fs.writeFileSync(filePath, JSON.stringify(containerStatus, null, 2)); + logger.info(`Container states saved to ${filePath}`); + // TODO: Add logic + notification levels per service + } else { + logger.info("No state change detected, notifications not triggered."); + } + } catch (error: any) { + logger.error( + `Error fetching data: ${JSON.stringify(error)} \nStack trace: ${error.stack}`, + ); + } +}; + +export default fetchData; diff --git a/controllers/frontendConfiguration.js b/src/controllers/frontendConfiguration.ts similarity index 73% rename from controllers/frontendConfiguration.js rename to src/controllers/frontendConfiguration.ts index cdbee13..6a5a691 100644 --- a/controllers/frontendConfiguration.js +++ b/src/controllers/frontendConfiguration.ts @@ -1,18 +1,17 @@ -const fs = require("fs"); -const path = require("path"); -const dataPath = path.join(__dirname, "../data/frontendConfiguration.json"); -const logger = require("../utils/logger"); -const expression = +import fs from "fs"; +import logger from "../utils/logger"; +const dataPath: string = "./src/data/frontendConfiguration.json"; +const expression: string = "https?://(www.)?[-a-zA-Z0-9@:%._+~#=]{1,256}.[a-zA-Z0-9()]{1,6}([-a-zA-Z0-9()@:%_+.~#?&//=]*)"; const regex = new RegExp(expression); /////////////////////////////////////////////////////////////// // Hide Containers: -async function hideContainer(containerName) { +async function hideContainer(containerName: string) { try { let data = await readData(); const containerIndex = data.findIndex( - (container) => container.name === containerName, + (container: any) => container.name === containerName, ); if (containerIndex !== -1) { @@ -22,17 +21,17 @@ async function hideContainer(containerName) { data.push({ name: containerName, hidden: true }); await saveData(data); } - } catch (error) { + } catch (error: any) { logger.error(error); throw new Error(error); } } -async function unhideContainer(containerName) { +async function unhideContainer(containerName: string) { try { let data = await readData(); const containerIndex = data.findIndex( - (container) => container.name === containerName, + (container: any) => container.name === containerName, ); if (containerIndex !== -1) { @@ -40,7 +39,7 @@ async function unhideContainer(containerName) { await saveData(data); cleanupData(); } - } catch (error) { + } catch (error: any) { logger.error(error); throw new Error(error); } @@ -48,11 +47,11 @@ async function unhideContainer(containerName) { /////////////////////////////////////////////////////////////// // Tag containers -async function addTagToContainer(containerName, tag) { +async function addTagToContainer(containerName: string, tag: string) { try { let data = await readData(); const containerIndex = data.findIndex( - (container) => container.name === containerName, + (container: any) => container.name === containerName, ); if (containerIndex !== -1) { @@ -65,27 +64,27 @@ async function addTagToContainer(containerName, tag) { data.push({ name: containerName, tags: [tag] }); await saveData(data); } - } catch (error) { + } catch (error: any) { logger.error(error); throw new Error(error); } } -async function removeTagFromContainer(containerName, tag) { +async function removeTagFromContainer(containerName: string, tag: string) { try { let data = await readData(); const containerIndex = data.findIndex( - (container) => container.name === containerName, + (container: any) => container.name === containerName, ); if (containerIndex !== -1 && data[containerIndex].tags) { data[containerIndex].tags = data[containerIndex].tags.filter( - (t) => t !== tag, + (t: any) => t !== tag, ); await saveData(data); cleanupData(); } - } catch (error) { + } catch (error: any) { logger.error(error); throw new Error(error); } @@ -93,11 +92,11 @@ async function removeTagFromContainer(containerName, tag) { /////////////////////////////////////////////////////////////// // Pin containers -async function pinContainer(containerName) { +async function pinContainer(containerName: string) { try { - let data = await readData(); - const containerIndex = data.findIndex( - (container) => container.name === containerName, + let data: any = await readData(); + const containerIndex: number = data.findIndex( + (container: any) => container.name === containerName, ); if (containerIndex !== -1) { @@ -107,17 +106,17 @@ async function pinContainer(containerName) { data.push({ name: containerName, pinned: true }); await saveData(data); } - } catch (error) { + } catch (error: any) { logger.error(error); throw new Error(error); } } -async function unpinContainer(containerName) { +async function unpinContainer(containerName: string) { try { let data = await readData(); const containerIndex = data.findIndex( - (container) => container.name === containerName, + (container: any) => container.name === containerName, ); if (containerIndex !== -1) { @@ -125,7 +124,7 @@ async function unpinContainer(containerName) { await saveData(data); cleanupData(); } - } catch (error) { + } catch (error: any) { logger.error(error); throw new Error(error); } @@ -133,12 +132,12 @@ async function unpinContainer(containerName) { /////////////////////////////////////////////////////////////// // Add/remove link from containers -async function setLink(containerName, link) { +async function setLink(containerName: string, link: string) { if (link.match(regex)) { try { - let data = await readData(); - const containerIndex = data.findIndex( - (container) => container.name === containerName, + let data: any = await readData(); + const containerIndex: any = data.findIndex( + (container: any) => container.name === containerName, ); if (containerIndex !== -1) { @@ -148,7 +147,7 @@ async function setLink(containerName, link) { data.push({ name: containerName, link: `${link}` }); await saveData(data); } - } catch (error) { + } catch (error: any) { logger.error(error); throw new Error(error); } @@ -158,11 +157,11 @@ async function setLink(containerName, link) { } } -async function removeLink(containerName) { +async function removeLink(containerName: string) { try { let data = await readData(); const containerIndex = data.findIndex( - (container) => container.name === containerName, + (container: any) => container.name === containerName, ); if (containerIndex !== -1) { @@ -170,7 +169,7 @@ async function removeLink(containerName) { await saveData(data); cleanupData(); } - } catch (error) { + } catch (error: any) { logger.error(error); throw new Error(error); } @@ -178,11 +177,11 @@ async function removeLink(containerName) { /////////////////////////////////////////////////////////////// // Add/remove icon from containers -async function setIcon(containerName, icon, custom) { +async function setIcon(containerName: string, icon: string, custom: boolean) { try { let data = await readData(); - const containerIndex = data.findIndex( - (container) => container.name === containerName, + const containerIndex: number = data.findIndex( + (container: any) => container.name === containerName, ); if (custom === true) { @@ -202,17 +201,17 @@ async function setIcon(containerName, icon, custom) { await saveData(data); } } - } catch (error) { + } catch (error: any) { logger.error(error); throw new Error(error); } } -async function removeIcon(containerName) { +async function removeIcon(containerName: string) { try { let data = await readData(); const containerIndex = data.findIndex( - (container) => container.name === containerName, + (container: any) => container.name === containerName, ); if (containerIndex !== -1) { @@ -220,7 +219,7 @@ async function removeIcon(containerName) { await saveData(data); cleanupData(); } - } catch (error) { + } catch (error: any) { logger.error(error); throw new Error(error); } @@ -232,7 +231,7 @@ async function readData() { try { const data = await fs.promises.readFile(dataPath, "utf-8"); return JSON.parse(data); - } catch (error) { + } catch (error: any) { console.error("readData"); if (error.code === "ENOENT") { await saveData([]); @@ -243,7 +242,7 @@ async function readData() { } } -async function saveData(data) { +async function saveData(data: any) { try { await fs.promises.writeFile( dataPath, @@ -251,7 +250,7 @@ async function saveData(data) { "utf-8", ); logger.info("Succesfully wrote to file"); - } catch (error) { + } catch (error: any) { logger.error(error); } } @@ -276,12 +275,12 @@ async function cleanupData() { } await saveData(cleanedData); - } catch (error) { + } catch (error: any) { logger.error(error); } } -module.exports = { +export { hideContainer, unhideContainer, addTagToContainer, diff --git a/src/controllers/highAvailability.ts b/src/controllers/highAvailability.ts new file mode 100644 index 0000000..e855757 --- /dev/null +++ b/src/controllers/highAvailability.ts @@ -0,0 +1,274 @@ +import logger from "../utils/logger"; +import fs from "fs"; +import chokidar from "chokidar"; +import path from "path"; +import { promisify } from "util"; + +const sleep = promisify(setTimeout); + +interface HighAvailabilityConfig { + active: boolean; + master: boolean; + nodes: string[]; +} + +interface Node { + ip: string; + id: number; +} + +interface HaNodeConfig { + master: string; +} + +interface NodeCache { + [nodes: string]: Node; +} + +const haMasterPath: string = "./src/data/highAvailability.json"; +const haNodePath: string = "./src/data/haNode.json"; +const nodeCachePath: string = "./src/data/nodeCache.json"; +const useUnsafeConnection = process.env.HA_UNSAFE || "false"; +const lockFilePath: string = "./src/data/ha.lock"; + +const configFiles: string[] = [ + "./src/data/dockerConfig.json", + "./src/data/states.json", + "./src/data/template.json", + "./src/data/frontendConfiguration.json", + "./src/data/nodeCache.json", + "./src/data/usePassword.txt", + "./src/data/password.json", +]; + +async function acquireLock(): Promise { + while (fs.existsSync(lockFilePath)) { + logger.warn("Lock file exists, waiting..."); + await sleep(100); + } + + try { + await fs.promises.writeFile(lockFilePath, "locked", { flag: "wx" }); + logger.debug("Lock acquired."); + } catch (error) { + logger.error(`Error acquiring lock: ${(error as Error).message}`); + throw new Error("Failed to acquire lock."); + } +} + +async function releaseLock(): Promise { + try { + if (fs.existsSync(lockFilePath)) { + await fs.promises.unlink(lockFilePath); + logger.debug("Lock released."); + } + } catch (error) { + logger.error(`Error releasing lock: ${(error as Error).message}`); + } +} + +async function writeConfig( + data: HighAvailabilityConfig | NodeCache | HaNodeConfig, + filePath: string, +): Promise { + await acquireLock(); + try { + logger.debug(`Writing ${filePath}`); + const dirPath: string = path.dirname(filePath); + await fs.promises.mkdir(dirPath, { recursive: true }); + + const jsonData = JSON.stringify(data, null, 2); + await fs.promises.writeFile(filePath, jsonData); + + logger.debug(`${filePath} has been written.`); + } catch (error) { + logger.error(`Error writing config: ${(error as Error).message}`); + } finally { + await releaseLock(); + } +} + +async function readConfig(): Promise { + await acquireLock(); + try { + logger.debug("Reading HA-Config"); + const data: HighAvailabilityConfig = JSON.parse( + fs.readFileSync(haMasterPath, "utf-8"), + ); + return data; + } catch (error: any) { + logger.error(`Error reading HA-Config: ${(error as Error).message}`); + return null; + } finally { + await releaseLock(); + } +} + +async function prepareFilesForSync(): Promise> { + const fileData: Record = {}; + try { + for (const filePath of configFiles) { + const content = await fs.promises.readFile(filePath, "utf-8"); + fileData[filePath] = content; + } + } catch (error) { + logger.error(`Error preparing files for sync: ${(error as Error).message}`); + } + return fileData; +} + +async function checkApiReachable(node: string): Promise { + let nodeUrl = + useUnsafeConnection === "true" + ? `http://${node}/api/status` + : `https://${node}/api/status`; + + try { + const response = await fetch(nodeUrl); + if (!response.ok) { + logger.error(`Failed to reach node ${node}. Status: ${response.status}`); + return false; + } + + const data = await response.json(); + if (data.ApiReachable) { + logger.info(`Node ${node} is reachable.`); + return true; + } else { + logger.error(`Node ${node} is not reachable. ApiReachable: false`); + return false; + } + } catch (error) { + logger.error(`Error reaching node ${node}: ${(error as Error).message}`); + return false; + } +} + +async function synchronizeFilesWithNodes(): Promise { + const haConfig = await readConfig(); + + if (!haConfig || !haConfig.master || haConfig.nodes.length === 0) { + logger.warn("No slave nodes to synchronize with."); + return; + } + + const files = await prepareFilesForSync(); + + for (const node of haConfig.nodes) { + if (!(await checkApiReachable(node))) { + logger.warn( + `Skipping file sync with ${node} due to connectivity issues.`, + ); + continue; // Skip synchronization if the node is unreachable + } + + let nodeUrl = + useUnsafeConnection == "true" + ? `http://${node}/ha/sync` + : `https://${node}/ha/sync`; + + logger.info(`Synchronizing files with node: ${node}`); + + const response = await fetch(nodeUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ files }), + }); + + if (response.ok) { + logger.info(`Files synchronized successfully with node: ${node}`); + } else { + logger.error( + `Failed to synchronize files with node ${node}. Status: ${response.status}`, + ); + } + } +} + +function monitorConfigFiles(): void { + const watcher = chokidar.watch(configFiles, { persistent: true }); + + watcher + .on("change", async (filePath) => { + logger.info(`File changed: ${filePath}. Initiating synchronization.`); + await synchronizeFilesWithNodes(); + }) + .on("error", (error) => { + logger.error(`Error watching files: ${(error as Error).message}`); + }); + + logger.info("Started monitoring configuration files for changes."); +} + +async function startMasterNode() { + if (process.env.HA_MASTER == "true") { + if (!process.env.HA_MASTER_IP) { + logger.error( + "Master's IP is not set, please set the HA_MASTER_IP variable (example: 10.0.0.4:9876)", + ); + } else { + const haNodeConfig: HaNodeConfig = { + master: "HA_MASTER_IP", + }; + const haConfig: HighAvailabilityConfig = { + active: true, + master: true, + nodes: process.env.HA_NODE + ? process.env.HA_NODE.split(",").map((node) => node.trim()) + : [], + }; + + const nodeCache: NodeCache = process.env.HA_NODE + ? process.env.HA_NODE.split(",").reduce((cache, node, index) => { + const [ip, id] = node.trim().split(":"); + if (ip && id) { + cache[`node${index + 1}`] = { ip, id: parseInt(id, 10) }; + } + return cache; + }, {} as NodeCache) + : {}; + + logger.debug("Writing HA-Config(s)"); + await writeConfig(haConfig, haMasterPath); + await writeConfig(haNodeConfig, haNodePath); + await writeConfig(nodeCache, nodeCachePath); + + logger.info("Running startup sync..."); + await synchronizeFilesWithNodes(); + logger.info("Watching config files in ./data"); + monitorConfigFiles(); + } + } else { + logger.info("This is a slave node"); + } +} + +async function ensureFileExists( + filePath: string, + content: string, +): Promise { + await acquireLock(); + try { + const dirPath = path.dirname(filePath); + await fs.promises.mkdir(dirPath, { recursive: true }); + await fs.promises.writeFile(filePath, content, { flag: "w" }); + logger.info(`File created/updated: ${filePath}`); + } catch (error) { + logger.error( + `Error creating/updating file ${filePath}: ${(error as Error).message}`, + ); + } finally { + await releaseLock(); + } +} + +export { + HighAvailabilityConfig, + writeConfig, + readConfig, + prepareFilesForSync, + synchronizeFilesWithNodes, + monitorConfigFiles, + startMasterNode, + ensureFileExists, +}; diff --git a/src/controllers/notificationController.ts b/src/controllers/notificationController.ts new file mode 100644 index 0000000..e34eecd --- /dev/null +++ b/src/controllers/notificationController.ts @@ -0,0 +1,62 @@ +import notify from "../utils/notifications/_notify"; +import logger from "../utils/logger"; + +const notificationTypes = { + discord: !!process.env.DISCORD_WEBHOOK_URL, + email: !!( + process.env.EMAIL_SENDER && + process.env.EMAIL_RECIPIENT && + process.env.EMAIL_PASSWORD + ), + pushbullet: !!process.env.PUSHBULLET_ACCESS_TOKEN, + pushover: !!(process.env.PUSHOVER_API_TOKEN && process.env.PUSHOVER_USER_KEY), + slack: !!process.env.SLACK_WEBHOOK_UR, + telegram: !!(process.env.TELEGRAM_BOT_TOKEN && process.env.TELEGRAM_CHAT_ID), + whatsapp: !!(process.env.WHATSAPP_API_URL && process.env.WHATSAPP_RECIPIENT), + custom: !!process.env.CUSTOM_NOTIFICATION, + customList: process.env.CUSTOM_NOTIFICATION, +}; + +async function sendNotification(containerId: string) { + if (notificationTypes.discord) { + logger.debug(`Sending notification via discord (${containerId})`); + notify("discord", containerId); + } + if (notificationTypes.email) { + logger.debug(`Sending notification via E-Mail (${containerId})`); + notify("email", containerId); + } + if (notificationTypes.pushbullet) { + logger.debug(`Sending notification via Pushbullet (${containerId})`); + notify("pushbullet", containerId); + } + if (notificationTypes.pushover) { + logger.debug(`Sending notification via Pushover (${containerId})`); + notify("pushover", containerId); + } + if (notificationTypes.slack) { + logger.debug(`Sending notification via Slack (${containerId})`); + notify("slack", containerId); + } + if (notificationTypes.telegram) { + logger.debug(`Sending notification via Telegram (${containerId})`); + notify("slack", containerId); + } + if (notificationTypes.whatsapp) { + logger.debug(`Sending notification via Pushbullet (${containerId})`); + notify("whatsapp", containerId); + } + if (notificationTypes.custom) { + const elements: undefined | string[] = notificationTypes.customList + ? notificationTypes.customList.split(",") + : undefined; + if (elements) { + elements.forEach((element) => { + logger.debug(`Sending custom notification ${element} (${containerId})`); + notify(`custom/${element}`, containerId); + }); + } else { + logger.error("Error getting custom notifications"); + } + } +} diff --git a/src/controllers/proxy.ts b/src/controllers/proxy.ts new file mode 100644 index 0000000..681adef --- /dev/null +++ b/src/controllers/proxy.ts @@ -0,0 +1,14 @@ +import { Application } from "express"; +import logger from "../utils/logger"; + +export default function trustedProxies(app: Application) { + const trusted: string = process.env.TRUSTED_PROXYS || ""; + + if (!trusted) { + logger.warn( + "No trusted Proxy configured, if ran behind a proxy please configure it according to the docs", + ); + } else { + app.set("trust proxy", trusted); + } +} diff --git a/controllers/scheduler.js b/src/controllers/scheduler.ts similarity index 56% rename from controllers/scheduler.js rename to src/controllers/scheduler.ts index e19b17e..763b67f 100644 --- a/controllers/scheduler.js +++ b/src/controllers/scheduler.ts @@ -1,15 +1,19 @@ -const fetchData = require("./fetchData"); -const logger = require("../utils/logger"); -const db = require("../config/db"); +import fetchData from "./fetchData"; +import logger from "../utils/logger"; +import db from "../config/db"; const regex = /(\d{1,5})([smh])/g; let fetchInterval = 5 * 60 * 1000; // Fetch data every 5 minutes by default -let intervalId; +const cleanupInterval = 24 * 60 * 60 * 1000; // every 24hrs +let intervalId: NodeJS.Timeout; const scheduleFetch = () => { - fetchData().then(() => { + try { + fetchData(); cleanupOldEntries(); - }); + } catch (error: any) { + logger.error(`Error during scheduled fetch: ${error}`); + } intervalId = setInterval(() => { logger.info( @@ -18,18 +22,24 @@ const scheduleFetch = () => { fetchData(); }, fetchInterval); - cleanupIntervalId = setInterval( - () => { - cleanupOldEntries(); - }, - 24 * 60 * 60 * 1000, - ); + setInterval(() => { + cleanupOldEntries(); + }, cleanupInterval); logger.info(`Data fetching scheduled every ${fetchInterval / 1000} seconds.`); logger.info("Old entries cleanup scheduled every 24 hours."); + + // Additional 20-second interval to log process exit listeners, if any + setInterval(() => { + const exitListeners = process.listeners("exit"); + + if (exitListeners.length > 0) { + logger.info(`Exit listeners detected: ${exitListeners}`); + } + }, 20000); }; -const setFetchInterval = (newInterval) => { +const setFetchInterval = (newInterval: number) => { if (intervalId) { clearInterval(intervalId); logger.info("Cleared existing fetch interval."); @@ -39,8 +49,8 @@ const setFetchInterval = (newInterval) => { logger.info(`Fetch interval updated to ${fetchInterval / 1000} seconds.`); }; -const parseInterval = (interval) => { - const timeUnits = { +const parseInterval = (interval: string) => { + const timeUnits: { [key: string]: number } = { s: 1000, m: 60 * 1000, h: 60 * 60 * 1000, @@ -69,16 +79,11 @@ const cleanupOldEntries = async () => { Date.now() - 24 * 60 * 60 * 1000, ).toISOString(); try { - await db.run("DELETE FROM data WHERE timestamp < ?", twentyFourHoursAgo); + db.run("DELETE FROM data WHERE timestamp < ?", twentyFourHoursAgo, Error); logger.info("Old entries cleared from the database."); - } catch (error) { - logger.error(`Error clearing old entries: ${error.message}`); + } catch (Error: any) { + logger.error(`Error clearing old entries: ${Error.message}`); } }; -module.exports = { - scheduleFetch, - setFetchInterval, - parseInterval, - getCurrentSchedule, -}; +export { scheduleFetch, setFetchInterval, parseInterval, getCurrentSchedule }; diff --git a/middleware/usePassword.txt b/src/data/usePassword.txt similarity index 100% rename from middleware/usePassword.txt rename to src/data/usePassword.txt diff --git a/src/init.ts b/src/init.ts new file mode 100644 index 0000000..3979eb6 --- /dev/null +++ b/src/init.ts @@ -0,0 +1,47 @@ +import express, { Request, Response, NextFunction } from "express"; +import swaggerDocs from "./utils/swaggerDocs"; +import auth from "./routes/auth/routes"; +import data from "./routes/data/routes"; +import frontend from "./routes/frontendController/routes"; +import api from "./routes/getter/routes"; +import notificationService from "./routes/notifications/routes"; +import conf from "./routes/setter/routes"; +import authMiddleware from "./middleware/authMiddleware"; +import ha from "./routes/highavailability/routes"; +import trustedProxies from "./controllers/proxy"; +import { limiter } from "./middleware/rateLimiter"; +import { scheduleFetch } from "./controllers/scheduler"; +import cors from "cors"; +import { blockWhileLocked } from "./middleware/checkLock"; + +const initializeApp = (app: express.Application): void => { + app.use(cors()); + app.use(express.json()); + app.use("/api-docs", (req: Request, res: Response, next: NextFunction) => + next(), + ); + + swaggerDocs(app as any); + trustedProxies(app); // Configures proxies using CSV string + scheduleFetch(); + + app.use("/api", limiter, authMiddleware, blockWhileLocked, api); + app.use("/conf", limiter, authMiddleware, blockWhileLocked, conf); + app.use("/auth", limiter, authMiddleware, blockWhileLocked, auth); + app.use("/data", limiter, authMiddleware, blockWhileLocked, data); + app.use("/frontend", limiter, authMiddleware, blockWhileLocked, frontend); + app.use( + "/notification-service", + limiter, + authMiddleware, + blockWhileLocked, + notificationService, + ); + app.use("/ha", limiter, authMiddleware, ha); + + app.get("/", (req: Request, res: Response) => { + res.redirect("/api-docs"); + }); +}; + +export default initializeApp; diff --git a/src/middleware/authMiddleware.ts b/src/middleware/authMiddleware.ts new file mode 100644 index 0000000..8caad08 --- /dev/null +++ b/src/middleware/authMiddleware.ts @@ -0,0 +1,52 @@ +import bcrypt from "bcrypt"; +import fs from "fs"; +import { Request, Response, NextFunction } from "express"; +import logger from "../utils/logger"; + +const passwordFile = "./src/data/password.json"; +const passwordBool = "./src/data/usePassword.txt"; + +async function authMiddleware( + req: Request, + res: Response, + next: NextFunction, +): Promise { + try { + const authStatusData = await fs.promises.readFile(passwordBool, "utf8"); + const isAuthEnabled = authStatusData.trim() === "true"; + + if (!isAuthEnabled) { + logger.warn("You are not using authentication, please enable it."); + logger.debug("Authentication disabled, skipping login process..."); + return next(); + } + + const providedPassword = req.headers["x-password"]; + if (!providedPassword) { + logger.error("Password required - Denied"); + res.status(401).json({ message: "Password required" }); + return; + } + + const passwordData = await fs.promises.readFile(passwordFile, "utf8"); + const storedData = JSON.parse(passwordData); + + const passwordMatch = await bcrypt.compare( + providedPassword as string, + storedData.hash, + ); + if (!passwordMatch) { + logger.error("Invalid Password - Denied access"); + res.status(401).json({ message: "Invalid password" }); + return; + } + + logger.debug("Authentication succesfull"); + next(); + } catch (error: any) { + logger.error("Error in authMiddleware:", error); + res.status(500).json({ message: "Internal server error" }); + } +} + +export default authMiddleware; diff --git a/src/middleware/checkLock.ts b/src/middleware/checkLock.ts new file mode 100644 index 0000000..747889d --- /dev/null +++ b/src/middleware/checkLock.ts @@ -0,0 +1,19 @@ +import fs from "fs"; +import { Request, Response, NextFunction } from "express"; + +const lockFilePath = "./src/data/ha.lock"; + +export function blockWhileLocked( + req: Request, + res: Response, + next: NextFunction, +): void { + if (fs.existsSync(lockFilePath)) { + res.status(503).json({ + error: + "Service unavailable. The high-availability lock is currently active. Please try again later.", + }); + } else { + next(); + } +} diff --git a/middleware/rateLimiter.js b/src/middleware/rateLimiter.ts similarity index 100% rename from middleware/rateLimiter.js rename to src/middleware/rateLimiter.ts diff --git a/src/misc/createEnvFile.sh b/src/misc/createEnvFile.sh new file mode 100644 index 0000000..cbd8244 --- /dev/null +++ b/src/misc/createEnvFile.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +# Version +VERSION="$1" + +# Docker +if grep -q '/docker' /proc/1/cgroup 2>/dev/null || [ -f /.dockerenv ]; then + RUNNING_IN_DOCKER="true" +else + RUNNING_IN_DOCKER="false" +fi +echo " +{ + \"RUNNING_IN_DOCKER\": \"${RUNNING_IN_DOCKER}\", + \"HA_MASTER\": \"${HA_MASTER}\", + \"HA_MASTER_IP\": \"${HA_MASTER_IP}\", + \"HA_NODE\": \"${HA_NODE}\", + \"HA_UNSAFE\": \"${HA_UNSAFE}\", + \"DISCORD_WEBHOOK_URL\": \"${DISCORD_WEBHOOK_URL}\", + \"EMAIL_SENDER\": \"${EMAIL_SENDER}\", + \"EMAIL_RECIPIENT\": \"${EMAIL_RECIPIENT}\", + \"EMAIL_PASSWORD\": \"${EMAIL_PASSWORD}\", + \"EMAIL_SERVICE\": \"${EMAIL_SERVICE}\", + \"PUSHBULLET_ACCESS_TOKEN\": \"${PUSHBULLET_ACCESS_TOKEN}\", + \"PUSHOVER_USER_KEY\": \"${PUSHOVER_USER_KEY}\", + \"PUSHOVER_API_TOKEN\": \"${PUSHOVER_API_TOKEN}\", + \"SLACK_WEBHOOK_URL\": \"${SLACK_WEBHOOK_URL}\", + \"TELEGRAM_BOT_TOKEN\": \"${TELEGRAM_BOT_TOKEN}\", + \"TELEGRAM_CHAT_ID\": \"${TELEGRAM_CHAT_ID}\", + \"WHATSAPP_API_URL\": \"${WHATSAPP_API_URL}\", + \"WHATSAPP_RECIPIENT\": \"${WHATSAPP_RECIPIENT}\", + \"CUSTOM_NOTIFICATION\": \"${CUSTOM_NOTIFICATION}\" +} +" > /api/src/data/variables.conf diff --git a/src/misc/dependencyGraphs/mermaid-all.txt b/src/misc/dependencyGraphs/mermaid-all.txt new file mode 100644 index 0000000..7e77f2c --- /dev/null +++ b/src/misc/dependencyGraphs/mermaid-all.txt @@ -0,0 +1,106 @@ +flowchart LR + +0["server.ts"] +subgraph 1["controllers"] +2["highAvailability.ts"] +3["scheduler.ts"] +6["fetchData.ts"] +K["frontendConfiguration.ts"] +end +subgraph 4["config"] +5["db.ts"] +19["swaggerConfig.ts"] +end +subgraph 7["utils"] +8["containerService.ts"] +9["dockerClient.ts"] +N["connectionChecker.ts"] +P["extractHostData.ts"] +Q["writeOfflineLog.ts"] +subgraph V["notifications"] +W["_notify.ts"] +X["discord.ts"] +Y["_template.ts"] +Z["email.ts"] +10["pushbullet.ts"] +11["pushover.ts"] +12["slack.ts"] +13["telegram.ts"] +14["whatsapp.ts"] +end +end +subgraph A["middleware"] +B["authMiddleware.ts"] +C["rateLimiter.ts"] +end +subgraph D["routes"] +subgraph E["auth"] +F["routes.ts"] +end +subgraph G["data"] +H["routes.ts"] +end +subgraph I["frontendController"] +J["routes.ts"] +end +subgraph L["getter"] +M["routes.ts"] +end +subgraph R["highavailability"] +S["routes.ts"] +end +subgraph T["notifications"] +U["routes.ts"] +end +subgraph 15["setter"] +16["routes.ts"] +end +end +O["net"] +subgraph 17["swagger"] +18["swaggerDocs.ts"] +end +0-->2 +0-->3 +0-->B +0-->C +0-->F +0-->H +0-->J +0-->M +0-->S +0-->U +0-->16 +0-->18 +3-->5 +3-->6 +6-->5 +6-->8 +8-->9 +H-->5 +J-->K +M-->3 +M-->N +M-->8 +M-->9 +M-->P +M-->Q +N-->O +S-->2 +U-->W +W-->X +W-->Z +W-->10 +W-->11 +W-->12 +W-->13 +W-->14 +X-->Y +Z-->Y +10-->Y +11-->Y +12-->Y +13-->Y +14-->Y +16-->3 +18-->19 diff --git a/src/misc/dependencyGraphs/mermaid-api.txt b/src/misc/dependencyGraphs/mermaid-api.txt new file mode 100644 index 0000000..e7c85cc --- /dev/null +++ b/src/misc/dependencyGraphs/mermaid-api.txt @@ -0,0 +1,32 @@ +flowchart LR + +subgraph 0["routes"] +subgraph 1["getter"] +2["routes.ts"] +end +end +subgraph 3["controllers"] +4["scheduler.ts"] +7["fetchData.ts"] +end +subgraph 5["config"] +6["db.ts"] +end +subgraph 8["utils"] +9["containerService.ts"] +A["dockerClient.ts"] +B["connectionChecker.ts"] +C["extractHostData.ts"] +D["writeOfflineLog.ts"] +end +2-->4 +2-->B +2-->9 +2-->A +2-->C +2-->D +4-->6 +4-->7 +7-->6 +7-->9 +9-->A diff --git a/misc/dependencyGraphs/mermaid-auth.txt b/src/misc/dependencyGraphs/mermaid-auth.txt similarity index 80% rename from misc/dependencyGraphs/mermaid-auth.txt rename to src/misc/dependencyGraphs/mermaid-auth.txt index e7ab066..aaeb683 100644 --- a/misc/dependencyGraphs/mermaid-auth.txt +++ b/src/misc/dependencyGraphs/mermaid-auth.txt @@ -2,7 +2,7 @@ flowchart LR subgraph 0["routes"] subgraph 1["auth"] -2["routes.js"] +2["routes.ts"] end end diff --git a/src/misc/dependencyGraphs/mermaid-conf.txt b/src/misc/dependencyGraphs/mermaid-conf.txt new file mode 100644 index 0000000..ba9ca66 --- /dev/null +++ b/src/misc/dependencyGraphs/mermaid-conf.txt @@ -0,0 +1,24 @@ +flowchart LR + +subgraph 0["routes"] +subgraph 1["setter"] +2["routes.ts"] +end +end +subgraph 3["controllers"] +4["scheduler.ts"] +7["fetchData.ts"] +end +subgraph 5["config"] +6["db.ts"] +end +subgraph 8["utils"] +9["containerService.ts"] +A["dockerClient.ts"] +end +2-->4 +4-->6 +4-->7 +7-->6 +7-->9 +9-->A diff --git a/misc/dependencyGraphs/mermaid-data.txt b/src/misc/dependencyGraphs/mermaid-data.txt similarity index 78% rename from misc/dependencyGraphs/mermaid-data.txt rename to src/misc/dependencyGraphs/mermaid-data.txt index e212edc..107d46a 100644 --- a/misc/dependencyGraphs/mermaid-data.txt +++ b/src/misc/dependencyGraphs/mermaid-data.txt @@ -2,10 +2,10 @@ flowchart LR subgraph 0["routes"] subgraph 1["data"] -2["routes.js"] +2["routes.ts"] end end subgraph 3["config"] -4["db.js"] +4["db.ts"] end 2-->4 diff --git a/misc/dependencyGraphs/mermaid-frontend.txt b/src/misc/dependencyGraphs/mermaid-frontend.txt similarity index 71% rename from misc/dependencyGraphs/mermaid-frontend.txt rename to src/misc/dependencyGraphs/mermaid-frontend.txt index 35b4e61..0334005 100644 --- a/misc/dependencyGraphs/mermaid-frontend.txt +++ b/src/misc/dependencyGraphs/mermaid-frontend.txt @@ -2,10 +2,10 @@ flowchart LR subgraph 0["routes"] subgraph 1["frontendController"] -2["routes.js"] +2["routes.ts"] end end subgraph 3["controllers"] -4["frontendConfiguration.js"] +4["frontendConfiguration.ts"] end 2-->4 diff --git a/src/misc/dependencyGraphs/mermaid-ha.txt b/src/misc/dependencyGraphs/mermaid-ha.txt new file mode 100644 index 0000000..ce15605 --- /dev/null +++ b/src/misc/dependencyGraphs/mermaid-ha.txt @@ -0,0 +1,11 @@ +flowchart LR + +subgraph 0["routes"] +subgraph 1["highavailability"] +2["routes.ts"] +end +end +subgraph 3["controllers"] +4["highAvailability.ts"] +end +2-->4 diff --git a/src/misc/dependencyGraphs/mermaid-notificationService.txt b/src/misc/dependencyGraphs/mermaid-notificationService.txt new file mode 100644 index 0000000..cef6c2c --- /dev/null +++ b/src/misc/dependencyGraphs/mermaid-notificationService.txt @@ -0,0 +1,35 @@ +flowchart LR + +subgraph 0["routes"] +subgraph 1["notifications"] +2["routes.ts"] +end +end +subgraph 3["utils"] +subgraph 4["notifications"] +5["_notify.ts"] +6["discord.ts"] +7["_template.ts"] +8["email.ts"] +9["pushbullet.ts"] +A["pushover.ts"] +B["slack.ts"] +C["telegram.ts"] +D["whatsapp.ts"] +end +end +2-->5 +5-->6 +5-->8 +5-->9 +5-->A +5-->B +5-->C +5-->D +6-->7 +8-->7 +9-->7 +A-->7 +B-->7 +C-->7 +D-->7 diff --git a/src/misc/entrypoint.sh b/src/misc/entrypoint.sh new file mode 100755 index 0000000..ff5cc61 --- /dev/null +++ b/src/misc/entrypoint.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +VERSION="2.0.0" + +echo -e " +\033[1;32mWelcome to\033[0m + +\033[1;34m###### ###### #### ### ### #### ######### ###### #########\033[0m +\033[1;34m### ### ### ### ### ### ### ### ### ### ### ###\033[0m +\033[1;34m### ### ### ### ### ###### #### ### ### ### ###\033[0m +\033[1;34m### ### ### ### ### ### ### #### ### ############ ###\033[0m +\033[1;34m### ### ### ### ### ### ### #### ### ### ### ###\033[0m +\033[1;34m###### ###### #### ### ### #### ### ### ### ### \033[0m(\033[1;33mAPI - v${VERSION}\033[0m) + +\033[1;36mUseful links:\033[0m + +- Documentation: \033[1;32mhttps://outline.itsnik.de/s/dockstat\033[0m +- GitHub (Frontend): \033[1;32mhttps://github.com/its4nik/dockstat\033[0m +- GitHub (Backend): \033[1;32mhttps://github.com/its4nik/dockstatapi\033[0m +- API Documentation: \033[1;32mhttp://localhost:7000/api-docs\033[0m + +\033[1;35mSummary:\033[0m + +DockStat and DockStatAPI are 2 fully OpenSource projects, DockStatAPI is a simple but extensible API which allows queries via a REST endpoint. + +" + +bash "./createEnvFile.sh" "$VERSION" + +exec node src/server.js diff --git a/src/misc/minifyDist.sh b/src/misc/minifyDist.sh new file mode 100644 index 0000000..0c25617 --- /dev/null +++ b/src/misc/minifyDist.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +dist="$(pwd)/dist" + +run_script() { + npx uglifyjs --no-annotations --in-situ "$1" > /dev/null + echo "✔️ Minified : $(basename "$1")" +} + +if [ -d "$dist" ]; then + echo "::: Dist directory exists." +else + echo "::: Dist does not exist... Running npx tsc" + npx tsc +fi + +max_jobs=$(nproc) +job_count=0 + +for file in $(find "$dist" -type f); do + run_script "$file" & + ((job_count++)) + + if ((job_count >= max_jobs)); then + wait + job_count=0 + fi +done + +wait + +echo + +if [[ $1 == "--build-only" ]]; then + exit 0 +fi + +node dist/server.js diff --git a/src/routes/auth/routes.ts b/src/routes/auth/routes.ts new file mode 100644 index 0000000..4af1388 --- /dev/null +++ b/src/routes/auth/routes.ts @@ -0,0 +1,174 @@ +import { Router, Request, Response } from "express"; +import bcrypt from "bcrypt"; +import fs from "fs/promises"; +import logger from "../../utils/logger"; +const passwordFile: string = "./src/data/password.json"; +const passwordBool: string = "./src/data/usePassword.txt"; +const saltRounds: number = 10; +const router: Router = Router(); + +let passwordData: { + hash: string; + salt: string; +}; + +async function authEnabled(): Promise { + let isAuthEnabled: boolean = false; + let data: string = ""; + try { + data = await fs.readFile(passwordBool, "utf8"); + isAuthEnabled = data.trim() === "true"; + return isAuthEnabled; + } catch (error: any) { + logger.error("Error reading file: ", error); + return isAuthEnabled; + } +} + +async function readPasswordFile() { + let data: string = ""; + try { + data = await fs.readFile(passwordFile, "utf8"); + return data; + } catch (error: any) { + logger.error("Could not read saved password: ", error); + return data; + } +} + +async function writePasswordFile(passwordData: string) { + try { + await fs.writeFile(passwordFile, passwordData); + setTrue(); + logger.debug("Authentication enabled"); + return "Authentication enabled"; + } catch (error: any) { + logger.error("Error writing password file:", error); + return error; + } +} + +async function setTrue() { + try { + await fs.writeFile(passwordBool, "true", "utf8"); + logger.info(`Enabled authentication`); + return; + } catch (error: any) { + logger.error("Error writing to the file:", error); + return; + } +} + +async function setFalse() { + try { + await fs.writeFile(passwordBool, "false", "utf8"); + logger.info(`Disabled authentication`); + return; + } catch (error: any) { + logger.error("Error writing to the file:", error); + return; + } +} + +/** + * @swagger + * /auth/enable: + * post: + * summary: Enable authentication by setting a password + * tags: [Authentication] + * parameters: + * - name: password + * in: query + * required: true + * responses: + * 200: + * description: Authentication enabled. + * 400: + * description: Password is required. + * 500: + * description: Error saving password. + */ +router.post("/enable", async (req: Request, res: Response): Promise => { + try { + const password = req.query.password as string; + + if (await authEnabled()) { + logger.error( + "Password Authentication is already enabled, please deactivate it first", + ); + res.status(401).json({ + message: + "Password Authentication is already enabled, please deactivate it first", + }); + return; + } + + if (!password) { + logger.error("Password is required"); + res.status(400).json({ message: "Password is required" }); + return; + } + + const salt = await bcrypt.genSalt(saltRounds); + const hash = await bcrypt.hash(password, salt); + + const passwordData = { hash, salt }; + writePasswordFile(JSON.stringify(passwordData)); + + res + .status(200) + .json({ message: "Password Authentication enabled successfully" }); + } catch (error) { + logger.error(`Error enabling password authentication: ${error}`); + res.status(500).json({ message: "An error occurred" }); + } +}); + +/** + * @swagger + * /auth/disable: + * post: + * summary: Disable authentication by providing the existing password + * tags: [Authentication] + * parameters: + * - name: password + * in: query + * required: true + * responses: + * 200: + * description: Authentication disabled. + * 400: + * description: Password is required. + * 401: + * description: Invalid password. + * 500: + * description: Error disabling authentication. + */ +router.post("/disable", async (req: Request, res: Response): Promise => { + try { + const password = req.query.password as string; + + if (!password) { + logger.error("Password is required!"); + res.status(400).json({ message: "Password is required" }); + return; + } + + const storedData = JSON.parse(await readPasswordFile()); + + const isPasswordValid = await bcrypt.compare(password, storedData.hash); + if (!isPasswordValid) { + logger.error("Invalid password"); + res.status(401).json({ message: "Invalid password" }); + return; + } + + await setFalse(); // Assuming this is an async function + res.status(200).json({ message: "Authentication disabled" }); + } catch (error) { + logger.error(`Error disabling authentication: ${error}`); + res.status(500).json({ message: "An error occurred" }); + } +}); + +export default router; diff --git a/src/routes/data/routes.ts b/src/routes/data/routes.ts new file mode 100644 index 0000000..0e9a6e3 --- /dev/null +++ b/src/routes/data/routes.ts @@ -0,0 +1,201 @@ +import express from "express"; +const router = express.Router(); +import db from "../../config/db"; +import logger from "../../utils/logger"; + +interface DataRow { + info: string; +} + +function formatRows(rows: DataRow[]): Record { + return rows.reduce( + (acc: Record, row, index: number): Record => { + acc[index] = JSON.parse(row.info); + return acc; + }, + {}, + ); +} + +/** + * @swagger + * /data/latest: + * get: + * summary: Retrieve the latest container statistics for a specific host + * tags: [Database queries] + * responses: + * 200: + * description: A JSON object containing the latest container statistics for the specified host. + * content: + * application/json: + * schema: + * type: object + * properties: + * Fin-2: + * type: array + * items: + * type: object + * properties: + * name: + * type: string + * description: The name of the container + * example: "Container A" + * id: + * type: string + * description: Unique identifier for the container + * example: "abcd1234" + * hostName: + * type: string + * description: Name of the host system running this container + * example: "Fin-2" + * state: + * type: string + * description: Current state of the container + * example: "running" + * cpu_usage: + * type: number + * description: CPU usage percentage for this container + * example: 30 + * mem_usage: + * type: number + * description: Memory usage in bytes + * example: 2097152 + * mem_limit: + * type: number + * description: Memory limit in bytes set for this container + * example: 8123764736 + * net_rx: + * type: number + * description: Total network received bytes since container start + * example: 151763111 + * net_tx: + * type: number + * description: Total network transmitted bytes since container start + * example: 7104386 + * current_net_rx: + * type: number + * description: Current received bytes in the recent period + * example: 1048576 + * current_net_tx: + * type: number + * description: Current transmitted bytes in the recent period + * example: 524288 + * networkMode: + * type: string + * description: Networking mode for the container + * example: "bridge" + */ +router.get("/latest", (req, res) => { + db.get( + "SELECT info FROM data ORDER BY timestamp DESC LIMIT 1", + (error, row: any) => { + if (error) { + logger.error("Error fetching latest data:", error.message); + return res.status(500).json({ error: "Internal server error" }); + } + + if (!row) { + logger.warn("No data available for /data/latest"); + return res.status(404).json({ error: "No data available" }); + } + + logger.debug("Fetching /data/latest"); + res.json(JSON.parse(row.info)); + }, + ); +}); + +/** + * @swagger + * /data/time/24h: + * get: + * summary: Retrieve container statistics entries from the last 24 hours + * tags: [Database queries] + * responses: + * 200: + * description: A numbered array of 'info' JSON objects from the last 24 hours. + * content: + * application/json: + * schema: + * type: object + * properties: + * 0: + * type: object + * description: Statistics for the first entry within 24 hours. + * properties: + * name: + * type: string + * example: "Container A" + * id: + * type: string + * example: "abcd1234" + * cpu_usage: + * type: number + * example: 30 + * mem_usage: + * type: number + * example: 2048 + * 1: + * type: object + * description: Statistics for the second entry within 24 hours. + * properties: + * name: + * type: string + * example: "Container B" + * id: + * type: string + * example: "efgh5678" + * cpu_usage: + * type: number + * example: 45 + * mem_usage: + * type: number + * example: 3072 + */ +router.get("/time/24h", (req, res) => { + const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); + db.all( + "SELECT info FROM data WHERE timestamp >= ?", + [oneDayAgo], + (error, rows: DataRow[]) => { + if (error) { + logger.error("Error fetching data from last 24 hours:", error.message); + return res.status(500).json({ error: "Internal server error" }); + } + logger.debug("Fetching /data/time/24h"); + res.json(formatRows(rows)); + }, + ); +}); + +/** + * @swagger + * /data/clear: + * delete: + * summary: Clear all container statistics entries from the database + * tags: [Database queries] + * responses: + * 200: + * description: A message indicating whether the database was cleared successfully. + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * description: Success message upon database clearance + * example: "Database cleared successfully." + */ +router.delete("/clear", (req, res) => { + db.run("DELETE FROM data", (err) => { + if (err) { + logger.error("Error clearing the database:", err.message); + return res.status(500).json({ error: "Internal server error" }); + } + logger.debug("Database cleared successfully"); + res.json({ message: "Database cleared successfully" }); + }); +}); + +export default router; diff --git a/routes/frontendController/routes.js b/src/routes/frontendController/routes.ts similarity index 97% rename from routes/frontendController/routes.js rename to src/routes/frontendController/routes.ts index de08c7a..fe5d841 100644 --- a/routes/frontendController/routes.js +++ b/src/routes/frontendController/routes.ts @@ -1,6 +1,7 @@ -const express = require("express"); +import express from "express"; +import logger from "../../utils/logger"; const router = express.Router(); -const { +import { hideContainer, unhideContainer, addTagToContainer, @@ -11,7 +12,7 @@ const { removeLink, setIcon, removeIcon, -} = require("../../controllers/frontendConfiguration"); +} from "../../controllers/frontendConfiguration"; /* ____ ___ ____ _____ @@ -67,9 +68,9 @@ router.post("/show/:containerName", async (req, res) => { const { containerName } = req.params; try { await unhideContainer(containerName); - res.json({ success: true, message: "Container unhidden successfully." }); - } catch (error) { - res.status(500).json({ success: false, error: error.message }); + res.status(200).json({ message: "Container unhidden successfully." }); + } catch (error: any) { + res.status(500).json({ error: error.message }); } }); @@ -126,7 +127,7 @@ router.post("/tag/:containerName/:tag", async (req, res) => { try { await addTagToContainer(containerName, tag); res.json({ success: true, message: "Tag added successfully." }); - } catch (error) { + } catch (error: any) { res.status(500).json({ success: false, error: error.message }); } }); @@ -178,7 +179,7 @@ router.post("/pin/:containerName", async (req, res) => { try { await pinContainer(containerName); res.json({ success: true, message: "Container pinned successfully." }); - } catch (error) { + } catch (error: any) { res.status(500).json({ success: false, error: error.message }); } }); @@ -236,7 +237,7 @@ router.post("/add-link/:containerName/:link", async (req, res) => { try { await setLink(containerName, link); res.json({ success: true, message: "Link added successfully." }); - } catch (error) { + } catch (error: any) { res.status(500).json({ success: false, error: error.message }); } }); @@ -304,7 +305,7 @@ router.post( await setIcon(containerName, icon, custom); res.json({ success: true, message: "Icon added successfully." }); - } catch (error) { + } catch (error: any) { res.status(500).json({ success: false, error: error.message }); } }, @@ -366,7 +367,7 @@ router.delete("/hide/:containerName", async (req, res) => { try { await hideContainer(target); res.json({ success: true, message: `Container, ${target}, hidden.` }); - } catch (error) { + } catch (error: any) { res.status(500).json({ success: false, error: error.message }); } }); @@ -424,7 +425,7 @@ router.delete("/remove-tag/:containerName/:tag", async (req, res) => { try { await removeTagFromContainer(containerName, tag); res.json({ success: true, message: "Tag removed successfully." }); - } catch (error) { + } catch (error: any) { res.status(500).json({ success: false, error: error.message }); } }); @@ -476,7 +477,7 @@ router.delete("/unpin/:containerName", async (req, res) => { try { await unpinContainer(containerName); res.json({ success: true, message: "Container unpinned successfully." }); - } catch (error) { + } catch (error: any) { res.status(500).json({ success: false, error: error.message }); } }); @@ -528,7 +529,7 @@ router.delete("/remove-link/:containerName", async (req, res) => { try { await removeLink(containerName); res.json({ success: true, message: "Link removed successfully." }); - } catch (error) { + } catch (error: any) { res.status(500).json({ success: false, error: error.message }); } }); @@ -580,9 +581,9 @@ router.delete("/remove-icon/:containerName", async (req, res) => { try { await removeIcon(containerName); res.json({ success: true, message: "Icon removed successfully." }); - } catch (error) { + } catch (error: any) { res.status(500).json({ success: false, error: error.message }); } }); -module.exports = router; +export default router; diff --git a/routes/getter/routes.js b/src/routes/getter/routes.ts similarity index 68% rename from routes/getter/routes.js rename to src/routes/getter/routes.ts index 6c5fbc0..0f9883f 100644 --- a/routes/getter/routes.js +++ b/src/routes/getter/routes.ts @@ -1,16 +1,15 @@ -const extractRelevantData = require("../../utils/extractHostData"); -const express = require("express"); -const router = express.Router(); -const { - writeOfflineLog, - readOfflineLog, -} = require("../../utils/writeOfflineLog"); -const { getDockerClient } = require("../../utils/dockerClient"); -const { fetchAllContainers } = require("../../utils/containerService"); -const { getCurrentSchedule } = require("../../controllers/scheduler"); -const logger = require("../../utils/logger"); -const path = require("path"); -const fs = require("fs"); +import extractRelevantData from "../../utils/extractHostData"; +import { Router, Request, Response } from "express"; +import { writeOfflineLog, readOfflineLog } from "../../utils/writeOfflineLog"; +import getDockerClient from "../../utils/dockerClient"; +import fetchAllContainers from "../../utils/containerService"; +import { getCurrentSchedule } from "../../controllers/scheduler"; +import logger from "../../utils/logger"; +import fs from "fs"; +import checkReachability from "../../utils/connectionChecker"; +const configPath = "./src/data/dockerConfig.json"; +const router = Router(); +const userConf = "./src/data/user.conf"; /** * @swagger @@ -32,12 +31,65 @@ const fs = require("fs"); * type: string * example: ["local", "remote1"] */ +router.get("/hosts", (req: Request, res: Response) => { + logger.info(`Fetching config: ${configPath}`); + try { + const rawData = fs.readFileSync(configPath, "utf-8"); + const config = JSON.parse(rawData); + + if (!config.hosts) { + throw new Error("No hosts defined in configuration."); + } -router.get("/hosts", (req, res) => { - const config = require("../../config/dockerConfig.json"); - const hosts = config.hosts.map((host) => host.name); - logger.info("Fetching all available Docker hosts"); - res.status(200).json({ hosts }); + const hosts = config.hosts.map((host: any) => host.name); + logger.debug("Fetching all available Docker hosts"); + res.status(200).json({ hosts }); + } catch (error: any) { + logger.error("Error fetching hosts: " + error.message); + res.status(500).json({ error: "Failed to fetch Docker hosts" }); + } +}); + +/** + * @swagger + * /api/system: + * get: + * summary: Retrieve system configuration details + * tags: [Misc] + * responses: + * 200: + * description: A JSON object containing the system configuration details. + * content: + * application/json: + * schema: + * type: object + * description: The parsed configuration details. + * 500: + * description: An error occurred while fetching the system configuration. + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * description: Error message detailing the issue encountered. + */ +router.get("/system", (req: Request, res: Response) => { + logger.info(`Fetching ${userConf}`); + + try { + const rawData = fs.readFileSync(userConf, "utf8"); + const config = JSON.parse(rawData); + + if (!config) { + res.status(500).json({ error: `Error received empty ${userConf}` }); + } + res.status(200).json(config); + } catch (error: any) { + logger.error(`Could not fetch ${userConf}: ${error}`); + res.status(500).json({ error: `Failed to fetch ${userConf}` }); + } }); /** @@ -81,7 +133,7 @@ router.get("/hosts", (req, res) => { * type: string * description: Error message detailing the issue encountered. */ -router.get("/host/:hostName/stats", async (req, res) => { +router.get("/host/:hostName/stats", async (req: Request, res: Response) => { const hostName = req.params.hostName; logger.info(`Fetching stats for host: ${hostName}`); if (process.env.OFFLINE === "true") { @@ -96,7 +148,7 @@ router.get("/host/:hostName/stats", async (req, res) => { writeOfflineLog(JSON.stringify(relevantData)); res.status(200).json(relevantData); - } catch (error) { + } catch (error: any) { logger.error( `Error fetching stats for host: ${hostName} - ${error.message || "Unknown error"}`, ); @@ -180,12 +232,13 @@ router.get("/host/:hostName/stats", async (req, res) => { * type: string * description: Error message detailing the issue encountered. */ -router.get("/containers", async (req, res) => { +router.get("/containers", async (req: Request, res: Response) => { logger.info("Fetching all containers across all hosts"); try { const allContainerData = await fetchAllContainers(); + logger.debug("Fetched /api/containers"); res.status(200).json(allContainerData); - } catch (error) { + } catch (error: any) { logger.error(`Error fetching containers: ${error.message}`); res.status(500).json({ error: "Failed to fetch containers" }); } @@ -216,13 +269,13 @@ router.get("/containers", async (req, res) => { * type: string * description: Error message detailing the issue encountered. */ -router.get("/config", async (req, res) => { - const configPath = path.join(__dirname, "../../config/dockerConfig.json"); +router.get("/config", async (req: Request, res: Response) => { try { const rawData = fs.readFileSync(configPath); const jsonData = JSON.parse(rawData.toString()); + logger.debug("Fetching /api/config"); res.status(200).json(jsonData); - } catch (error) { + } catch (error: any) { logger.error("Error loading dockerConfig.json: " + error.message); res.status(500).json({ error: "Failed to load Docker configuration" }); } @@ -246,8 +299,9 @@ router.get("/config", async (req, res) => { * type: integer * description: Current fetch interval in seconds. */ -router.get("/current-schedule", (req, res) => { +router.get("/current-schedule", (req: Request, res: Response) => { const currentSchedule = getCurrentSchedule(); + logger.debug("Fetching current shedule"); res.json(currentSchedule); }); @@ -255,23 +309,39 @@ router.get("/current-schedule", (req, res) => { * @swagger * /api/status: * get: - * summary: Check server status + * summary: Check the DockStatAPI and docker socket status of each host * tags: [Misc] - * description: Returns a 200 status with an "up" message to indicate the server is up and running. Used for Healthchecks + * description: Returns the status of the backend and online components, indicating which nodes are reachable or offline. * responses: * 200: - * description: Server is running + * description: Server and backend status * content: * application/json: * schema: * type: object * properties: - * status: - * type: string - * example: "up" + * backendReachable: + * type: boolean + * example: true + * online: + * type: object + * properties: + * Host-1: + * type: boolean + * example: true + * Host-2: + * type: boolean + * example: false */ -router.get("/status", (req, res) => { - res.status(200).json({ status: "up" }); + +router.get("/status", async (req: Request, res: Response) => { + logger.debug("Fetching /api/status"); + try { + let jsonData = await checkReachability(); + res.status(200).json(jsonData); + } catch (error: any) { + logger.error(`Error while fetching data: ${error}`); + } }); /** @@ -316,19 +386,27 @@ router.get("/status", (req, res) => { * type: string * description: Error message */ -router.get("/frontend-config", (req, res) => { - const configPath = path.join( - __dirname, - "../../data/frontendConfiguration.json", - ); +router.get("/frontend-config", (req: Request, res: Response) => { + const configPath: string = "./src/data/frontendConfiguration.json"; + + fs.stat(configPath, (exists) => { + if (exists == null) { + logger.debug(`${configPath} exists, trying to read it`); + } else if (exists.code === "ENOENT") { + logger.warn(`${configPath} doesn't exist, trying to create it`); + fs.promises.writeFile(configPath, JSON.stringify([], null, 2), "utf-8"); + } + }); + try { const rawData = fs.readFileSync(configPath); const jsonData = JSON.parse(rawData.toString()); + res.status(200).json(jsonData); - } catch (error) { + } catch (error: any) { logger.error("Error loading frontendConfiguration.json: " + error.message); res.status(500).json({ error: "Failed to load Frontend configuration" }); } }); -module.exports = router; +export default router; diff --git a/src/routes/highavailability/routes.ts b/src/routes/highavailability/routes.ts new file mode 100644 index 0000000..bc4cb79 --- /dev/null +++ b/src/routes/highavailability/routes.ts @@ -0,0 +1,92 @@ +// File: /src/routes/ha/routes.ts +import { Router, Request, Response } from "express"; +import logger from "../../utils/logger"; +import { + readConfig, + synchronizeFilesWithNodes, + prepareFilesForSync, + HighAvailabilityConfig, + ensureFileExists, +} from "../../controllers/highAvailability"; + +interface SyncRequestBody { + files: Record; +} + +const router = Router(); + +/** + * @swagger + * /ha/config: + * get: + * summary: Retrieve the High Availability Config + * tags: [High Availability] + * responses: + * 200: + * description: A JSON object containing the config. + */ +router.get("/config", async (req: Request, res: Response) => { + logger.info("Getting the HA-Config"); + const data = await readConfig(); + res.status(200).json(data); +}); + +/** + * @swagger + * /ha/sync: + * post: + * summary: Synchronize configuration files from master node. + * tags: [High Availability] + * responses: + * 200: + * description: Files synchronized successfully. + */ +router.post( + "/sync", + async ( + req: Request<{}, {}, SyncRequestBody>, + res: Response, + ): Promise => { + try { + const { files } = req.body; + + if (!files || typeof files !== "object") { + const errorMsg = + "Invalid request: 'files' object is missing or invalid."; + logger.error(errorMsg); + res.status(400).json({ message: errorMsg }); + return; + } + + logger.info("Received synchronization request from master node."); + + for (const [filePath, content] of Object.entries(files)) { + await ensureFileExists(filePath, content); + } + + logger.info("Synchronization completed successfully."); + res.status(200).json({ message: "Synchronization completed." }); + } catch (error) { + logger.error(`Error during synchronization: ${(error as Error).message}`); + res.status(500).json({ message: "Synchronization failed." }); + } + }, +); + +/** + * @swagger + * /ha/prepare-sync: + * get: + * summary: Prepare files for synchronization. + * tags: [High Availability] + * responses: + * 200: + * description: A JSON object containing files to sync. + */ +router.get("/prepare-sync", async (req: Request, res: Response) => { + logger.info("Preparing files for synchronization."); + const fileData = await prepareFilesForSync(); + res.status(200).json(fileData); +}); + +export default router; diff --git a/routes/notifications/routes.js b/src/routes/notifications/routes.ts similarity index 73% rename from routes/notifications/routes.js rename to src/routes/notifications/routes.ts index 592ab63..262d48f 100644 --- a/routes/notifications/routes.js +++ b/src/routes/notifications/routes.ts @@ -1,13 +1,26 @@ -const express = require("express"); -const router = express.Router(); -const logger = require("../../utils/logger"); -const path = require("path"); -const fs = require("fs"); -const notify = require("../../utils/notifications/_notify"); -const dataTemplate = path.join( - __dirname, - "../../utils/notifications/data/template.json", -); +import { Request, Response, Router } from "express"; +import logger from "../../utils/logger"; +import fs from "fs"; +import notify from "../../utils/notifications/_notify"; +const dataTemplate = "./src/data/template.json"; +const router = Router(); + +/////////// +// Will be moved! + +interface TemplateData { + text: string; +} + +function isTemplateData(data: any): data is TemplateData { + return ( + data !== null && typeof data === "object" && typeof data.text === "string" + ); +} + +// Will be moved +/////////// + /** * @swagger * /notification-service/get-template: @@ -39,7 +52,7 @@ const dataTemplate = path.join( * type: string * description: Error message */ -router.get("/get-template", (req, res) => { +router.get("/get-template", (req: Request, res: Response) => { fs.readFile(dataTemplate, "utf-8", (error, data) => { if (error) { logger.error("Errored opening:", error); @@ -84,23 +97,28 @@ router.get("/get-template", (req, res) => { * type: string * description: Error message */ -router.post("/set-template", (req, res) => { - const newData = req.body; +router.post("/set-template", (req: Request, res: Response): void => { + const newData: TemplateData = req.body; + + if (!isTemplateData(newData)) { + res.status(400).json({ + message: "Invalid input format. Expected JSON with a 'text' field.", + }); + return; + } - fs.writeFile( - dataTemplate, - JSON.stringify(newData, null, 2), - "utf-8", - (error) => { - if (error) { - logger.error("Errored writing to file:", error); - return res - .status(500) - .json({ message: `Error writing to file: ${error}` }); - } + fs.promises + .writeFile(dataTemplate, JSON.stringify(newData, null, 2), "utf-8") + .then(() => { + logger.info("Template updated successfully."); res.json({ message: "Template updated successfully." }); - }, - ); + }) + .catch((error) => { + logger.error("Error writing to file: " + error.message); + res + .status(500) + .json({ message: `Error writing to file: ${error.message}` }); + }); }); /** @@ -146,14 +164,14 @@ router.post("/set-template", (req, res) => { * message: * type: string */ -router.post("/test/:type/:containerId", async (req, res) => { +router.post("/test/:type/:containerId", async (req: Request, res: Response) => { const { type, containerId } = req.params; try { await notify(type, containerId); res.json({ success: true, message: `Sent test notification to ${type}` }); - } catch (error) { + } catch (error: any) { res.json({ success: false, message: `Errored: ${error}` }); } }); -module.exports = router; +export default router; diff --git a/src/routes/setter/routes.ts b/src/routes/setter/routes.ts new file mode 100644 index 0000000..fcffeef --- /dev/null +++ b/src/routes/setter/routes.ts @@ -0,0 +1,180 @@ +import { setFetchInterval, parseInterval } from "../../controllers/scheduler"; +import logger from "../../utils/logger"; +import { Router, Request, Response } from "express"; +import fs from "fs"; + +const router = Router(); +const configPath: string = "./src/data/dockerConfig.json"; + +interface Host { + name: string; + url: string; + port: string; +} + +interface DockerConfig { + hosts: Host[]; +} + +/** + * @swagger + * /conf/addHost: + * put: + * summary: Add a new host to the Docker configuration + * tags: [Configuration] + * parameters: + * - name: name + * in: query + * required: true + * description: The name of the new host. + * - name: url + * in: query + * required: true + * description: The URL of the new host. + * - name: port + * in: query + * required: true + * description: The port of the new host. + * responses: + * 200: + * description: Host added successfully. + * 400: + * description: Bad request, invalid input. + * 500: + * description: An error occurred while adding the host. + */ + +router.put( + "/addHost", + async ( + req: Request< + unknown, + unknown, + unknown, + { name: string; url: string; port: string } + >, + res: Response, + ): Promise => { + const { name, url, port } = req.query; + + if (!name || !url || !port) { + res.status(400).json({ error: "Name, Port, and URL are required." }); + return; + } + + try { + const config: DockerConfig = JSON.parse( + fs.readFileSync(configPath, "utf-8"), + ); + + if (config.hosts.some((host) => host.name === name)) { + res.status(400).json({ error: "Host already exists." }); + return; + } + + config.hosts.push({ name, url, port }); + fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); + logger.info(`Added new host: ${name}`); + res.status(200).json({ message: "Host added successfully." }); + } catch (error: unknown) { + const err = error as Error; + logger.error("Error adding host: " + err.message); + res.status(500).json({ error: "Failed to add host." }); + } + }, +); + +/** + * @swagger + * /conf/scheduler: + * put: + * summary: Set fetch interval for data fetching + * tags: [Configuration] + * parameters: + * - name: interval + * in: query + * required: true + * description: The new interval for fetching data, e.g., "6h 20m", "300s". + * responses: + * 200: + * description: Fetch interval set successfully. + * 400: + * description: Invalid interval format or out of range. + */ +router.put("/scheduler", (req: any, res: any) => { + const interval = req.query.interval as string; + + try { + const newInterval = parseInterval(interval); + + if (newInterval < 5 * 60 * 1000 || newInterval > 6 * 60 * 60 * 1000) { + return res + .status(400) + .json({ error: "Interval must be between 5 minutes and 6 hours." }); + } + + setFetchInterval(newInterval); + res.json({ message: `Fetch interval set to ${interval}.` }); + } catch (error: unknown) { + const err = error as Error; + logger.error("Error setting fetch interval: " + err.message); + res.status(400).json({ error: "Invalid interval format." }); + } +}); + +/** + * @swagger + * /conf/removeHost: + * delete: + * summary: Remove a host from the Docker configuration + * tags: [Configuration] + * parameters: + * - name: hostName + * in: query + * required: true + * description: The name of the host to remove. + * responses: + * 200: + * description: Host removed successfully. + * 404: + * description: Host not found. + * 500: + * description: An error occurred while removing the host. + */ +router.delete("/removeHost", (req: Request, res: Response): void => { + const hostName = req.query.hostName as string; + + if (!hostName) { + res.status(400).json({ error: "Host name is required." }); + return; + } + + fs.promises + .readFile(configPath, "utf-8") + .then((rawData) => { + const config: DockerConfig = JSON.parse(rawData); + const hostIndex = config.hosts.findIndex( + (host) => host.name === hostName, + ); + + if (hostIndex === -1) { + res.status(404).json({ error: "Host not found." }); + return; + } + + config.hosts.splice(hostIndex, 1); + + return fs.promises + .writeFile(configPath, JSON.stringify(config, null, 2)) + .then(() => { + logger.info(`Removed host: ${hostName}`); + res.status(200).json({ message: "Host removed successfully." }); + }); + }) + .catch((error) => { + logger.error("Error removing host: " + (error as Error).message); + res.status(500).json({ error: "Failed to remove host." }); + }); +}); + +export default router; diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..4853204 --- /dev/null +++ b/src/server.ts @@ -0,0 +1,17 @@ +import express from "express"; +import logger from "./utils/logger"; +import initializeApp from "./init"; +import { startMasterNode } from "./controllers/highAvailability"; +import writeUserConf from "./config/hostsystem"; + +const app = express(); +const PORT: number = 9876; + +writeUserConf(); +initializeApp(app); + +app.listen(PORT, () => { + logger.info(`Server is running on http://localhost:${PORT}`); + logger.info(`Swagger docs available at http://localhost:${PORT}/api-docs`); + startMasterNode(); +}); diff --git a/src/utils/connectionChecker.ts b/src/utils/connectionChecker.ts new file mode 100644 index 0000000..c61ffeb --- /dev/null +++ b/src/utils/connectionChecker.ts @@ -0,0 +1,77 @@ +import * as fs from "fs"; +import * as net from "net"; +import logger from "../config/loggerConfig"; + +const filePath: string = "./src/data/dockerConfig.json"; + +interface Host { + name: string; + url: string; + port: string; +} + +interface StatusResponse { + ApiReachable: boolean; + online: { [key: string]: boolean }; +} + +async function checkHostStatus(hosts: Host[]): Promise { + const results: { [key: string]: boolean } = {}; + for (const host of hosts) { + const { name, url, port } = host; + + const isOnline = await checkPort(url, parseInt(port, 10)); + + results[name] = !!isOnline; + + if (results[name] == true) { + logger.debug(`${host.url}:${port} is online`); + } else { + logger.debug(`${host.url}:${port} is unreachable`); + } + } + + return { + ApiReachable: true, + online: results, + }; +} + +function checkPort(host: string, port: number): Promise { + return new Promise((resolve) => { + const socket = new net.Socket(); + socket.setTimeout(3000); + + socket.on("connect", () => { + socket.end(); + resolve(true); + }); + + socket.on("timeout", () => { + socket.destroy(); + resolve(false); + }); + + socket.on("error", () => { + socket.destroy(); + resolve(false); + }); + + socket.connect(port, host); + }); +} + +async function checkReachability(): Promise { + try { + const data = fs.readFileSync(filePath, "utf-8"); + const parsedData = JSON.parse(data); + const hosts: Host[] = parsedData.hosts; + return await checkHostStatus(hosts); + + } catch (error: any) { + logger.error(`Error reading file: ${error}`); + return undefined; + } +} + +export default checkReachability; diff --git a/src/utils/containerService.ts b/src/utils/containerService.ts new file mode 100644 index 0000000..afc035a --- /dev/null +++ b/src/utils/containerService.ts @@ -0,0 +1,134 @@ +import logger from "./logger"; +import { ContainerInfo, ContainerStats, ContainerInspectInfo } from "dockerode"; +import getDockerClient from "./dockerClient"; +import fs from "fs"; +const configPath = "./src/data/dockerConfig.json"; + +interface HostConfig { + name: string; + [key: string]: any; +} + +interface ContainerData { + name: string; + id: string; + hostName: string; + state: string; + cpu_usage: number; + mem_usage: number; + mem_limit: number; + net_rx: number; + net_tx: number; + current_net_rx: number; + current_net_tx: number; + networkMode: string; +} + +interface AllContainerData { + [hostName: string]: ContainerData[] | { error: string }; +} + +function loadConfig() { + try { + if (!fs.existsSync(configPath)) { + logger.warn(`Config file not found. Creating an empty file at ${configPath}`); + fs.writeFileSync(configPath, JSON.stringify({ "hosts": [] }, null, 2), "utf-8"); + } + + const configData = fs.readFileSync(configPath, "utf-8"); + logger.debug("Loaded " + configPath); + return JSON.parse(configData); + } catch (error: any) { + logger.error(`Failed to load config: ${error.message}`); + return null; + } +} + +async function fetchAllContainers(): Promise { + const config = loadConfig(); + if (!config || !config.hosts) { + logger.error("Invalid or missing host configuration."); + return {}; + } + + const allContainerData: AllContainerData = {}; + + for (const hostConfig of config.hosts as HostConfig[]) { + const hostName = hostConfig.name; + try { + const docker: any = getDockerClient(hostName); + logger.debug(`Now processing: ${hostName}`); + const containers: ContainerInfo[] = await docker.listContainers({ + all: true, + }); + + allContainerData[hostName] = await Promise.all( + containers.map(async (container) => { + try { + const containerInstance = docker.getContainer(container.Id); + const containerInfo: ContainerInspectInfo = + await containerInstance.inspect(); + const containerStats: ContainerStats = + await containerInstance.stats({ stream: false }); + + const cpuDelta = + containerStats.cpu_stats.cpu_usage.total_usage - + containerStats.precpu_stats.cpu_usage.total_usage; + const systemCpuDelta = + containerStats.cpu_stats.system_cpu_usage - + containerStats.precpu_stats.system_cpu_usage; + const cpuUsage = + systemCpuDelta > 0 + ? (cpuDelta / systemCpuDelta) * + containerStats.cpu_stats.online_cpus + : 0; + + return { + name: container.Names[0].replace("/", ""), + id: container.Id, + hostName, + state: container.State, + cpu_usage: cpuUsage * 1000000000, + mem_usage: containerStats.memory_stats.usage, + mem_limit: containerStats.memory_stats.limit, + net_rx: containerStats.networks?.eth0?.rx_bytes || 0, + net_tx: containerStats.networks?.eth0?.tx_bytes || 0, + current_net_rx: containerStats.networks?.eth0?.rx_bytes || 0, + current_net_tx: containerStats.networks?.eth0?.tx_bytes || 0, + networkMode: containerInfo.HostConfig.NetworkMode || "unknown", + }; + } catch (containerError: any) { + logger.error( + `Error fetching details for container ID: ${container.Id} on host: ${hostName} - ${containerError.message}`, + ); + return { + name: container.Names[0].replace("/", ""), + id: container.Id, + hostName, + state: container.State, + cpu_usage: 0, + mem_usage: 0, + mem_limit: 0, + net_rx: 0, + net_tx: 0, + current_net_rx: 0, + current_net_tx: 0, + networkMode: "unknown", + }; + } + }), + ); + } catch (error: any) { + logger.error( + `Error fetching containers for host: ${hostName} - ${error.message}. Stack: ${error.stack}`, + ); + allContainerData[hostName] = { + error: `Error fetching containers: ${error.message}`, + }; + } + } + + return allContainerData; +} + +export default fetchAllContainers; diff --git a/src/utils/createDependencyGraph.sh b/src/utils/createDependencyGraph.sh new file mode 100755 index 0000000..c822999 --- /dev/null +++ b/src/utils/createDependencyGraph.sh @@ -0,0 +1,37 @@ +#!/bin/bash +cd src || exit 1 +TMP=$(mktemp) + +cat ./server.ts | grep "./routes" | awk '{print $2,$4}' > $TMP + +spawn_worker(){ + local line="$1" + local target_route="$(echo "$line" | cut -d '"' -f2).ts" + local route=$(echo "$line" | awk '{print $1}') + + echo -e "\nRoute: $route \n${target_route}" + + npx depcruise \ + -p cli-feedback \ + -T mermaid \ + -x "../node_modules|logger|.dependency-cruiser|path|fs|net" \ + -f ./misc/dependencyGraphs/mermaid-${route}.txt \ + ${target_route} || exit 1 +} + +while read line; do + spawn_worker "$line" & +done < <(cat $TMP) + +npx depcruise \ + -p cli-feedback \ + -T mermaid \ + -x "../node_modules|logger|.dependency-cruiser|path|fs" \ + -f ./misc/dependencyGraphs/mermaid-all.txt \ + ./server.ts || exit 1 + +wait + +echo -e "\n========\n\n DONE\n\n========" + +exit 0 diff --git a/src/utils/dockerClient.ts b/src/utils/dockerClient.ts new file mode 100644 index 0000000..4cb3f70 --- /dev/null +++ b/src/utils/dockerClient.ts @@ -0,0 +1,54 @@ +// src/utils/dockerClient.ts +import Docker from "dockerode"; +import fs from "fs"; +import logger from "./logger"; + +interface DockerHostConfig { + name: string; + url: string; + port?: number; +} + +interface DockerConfig { + hosts: DockerHostConfig[]; +} + +function loadDockerConfig(): DockerConfig { + const configPath = "./src/data/dockerConfig.json"; + try { + const rawData = fs.readFileSync(configPath, "utf-8"); + logger.debug("Refreshed DockerConfig.json"); + return JSON.parse(rawData) as DockerConfig; + } catch (error: any) { + logger.error( + "Error loading dockerConfig.json: " + (error as Error).message, + ); + throw new Error("Failed to load Docker configuration"); + } +} + +function createDockerClient(hostConfig: DockerHostConfig): Docker { + logger.info( + `Creating Docker client for host: ${hostConfig.url} on port: ${hostConfig.port || 2375}`, + ); + return new Docker({ + host: hostConfig.url, + port: hostConfig.port || 2375, + protocol: "http", + }); +} + +const getDockerClient = (hostName: string): Docker => { + logger.debug(`Getting Docker Client for ${hostName}`); + const config = loadDockerConfig(); + const hostConfig = config.hosts.find((host) => host.name === hostName); + + if (!hostConfig) { + const errorMsg = `Docker host ${hostName} not found in configuration`; + logger.error(errorMsg); + throw new Error(errorMsg); + } + return createDockerClient(hostConfig); +}; + +export default getDockerClient; diff --git a/src/utils/extractHostData.ts b/src/utils/extractHostData.ts new file mode 100644 index 0000000..b6192ea --- /dev/null +++ b/src/utils/extractHostData.ts @@ -0,0 +1,57 @@ +interface Component { + Name: string; + Version: string; +} + +interface JsonData { + hostName: string; + info: { + ID: string; + Containers: number; + ContainersRunning: number; + ContainersPaused: number; + ContainersStopped: number; + Images: number; + OperatingSystem: string; + KernelVersion: string; + Architecture: string; + MemTotal: number; + NCPU: number; + }; + version: { + Components: Component[]; + }; +} + +type ComponentMap = Record; + +// Export the function with type annotations +function extractRelevantData(jsonData: JsonData) { + return { + hostName: jsonData.hostName, + info: { + ID: jsonData.info.ID, + Containers: jsonData.info.Containers, + ContainersRunning: jsonData.info.ContainersRunning, + ContainersPaused: jsonData.info.ContainersPaused, + ContainersStopped: jsonData.info.ContainersStopped, + Images: jsonData.info.Images, + OperatingSystem: jsonData.info.OperatingSystem, + KernelVersion: jsonData.info.KernelVersion, + Architecture: jsonData.info.Architecture, + MemTotal: jsonData.info.MemTotal, + NCPU: jsonData.info.NCPU, + }, + version: { + Components: jsonData.version.Components.reduce( + (acc, component) => { + acc[component.Name] = component.Version; + return acc; + }, + {}, + ), + }, + }; +} + +export default extractRelevantData; diff --git a/utils/logger.js b/src/utils/logger.ts similarity index 52% rename from utils/logger.js rename to src/utils/logger.ts index 9d25e5d..e69955a 100644 --- a/utils/logger.js +++ b/src/utils/logger.ts @@ -1,7 +1,7 @@ -const winston = require("winston"); -const loggerConfig = require("../config/loggerConfig"); +import winston, { transport } from "winston"; +import loggerConfig from "../config/loggerConfig"; -const transports = [new winston.transports.Console()]; +const transports: transport[] = [new winston.transports.Console()]; transports.push( new winston.transports.File({ @@ -15,4 +15,4 @@ const logger = winston.createLogger({ transports, }); -module.exports = logger; +export default logger; diff --git a/src/utils/notifications/_notify.ts b/src/utils/notifications/_notify.ts new file mode 100644 index 0000000..018b3dc --- /dev/null +++ b/src/utils/notifications/_notify.ts @@ -0,0 +1,85 @@ +import logger from "../../utils/logger"; +import { telegramNotification } from "./telegram"; +import { slackNotification } from "./slack"; +import { discordNotification } from "./discord"; +import { emailNotification } from "./email"; +import { whatsappNotification } from "./whatsapp"; +import { pushbulletNotification } from "./pushbullet"; +import { pushoverNotification } from "./pushover"; +import path from "path"; + +async function loadCustomNotification(scriptPath: string, containerId: string) { + try { + const absolutePath = path.resolve(__dirname, "./custom", scriptPath); + const customModule = await import(absolutePath); + + if (typeof customModule.default !== "function") { + const errorMsg = `The custom notification script at ${scriptPath} does not export a default function.`; + logger.error(errorMsg); + throw new Error(errorMsg); + } + + logger.debug(`Executing custom notification script: ${scriptPath}`); + await customModule.default(containerId); + } catch (error: any) { + logger.error( + `Failed to execute custom notification script (${scriptPath}): ${error.message}`, + ); + throw error; + } +} + +async function notify(type: string, containerId: string) { + if (!containerId) { + logger.error("Container ID is required."); + throw new Error("Container ID is required."); + } + + if (type.startsWith("custom/")) { + const scriptName = type.split("/")[1]; + if (!scriptName) { + const errorMsg = "Custom notification script name is invalid."; + logger.error(errorMsg); + throw new Error(errorMsg); + } + await loadCustomNotification(`${scriptName}.js`, containerId); + return; + } + + switch (type) { + case "telegram": + logger.debug("Sending Telegram notification..."); + await telegramNotification(containerId); + break; + case "slack": + logger.debug("Sending Slack notification..."); + await slackNotification(containerId); + break; + case "discord": + logger.debug("Sending Discord notification..."); + await discordNotification(containerId); + break; + case "email": + logger.debug("Sending Email notification..."); + await emailNotification(containerId); + break; + case "whatsapp": + logger.debug("Sending WhatsApp notification..."); + await whatsappNotification(containerId); + break; + case "pushbullet": + logger.debug("Sending Pushbullet notification..."); + await pushbulletNotification(containerId); + break; + case "pushover": + logger.debug("Sending Pushover notification..."); + await pushoverNotification(containerId); + break; + default: + const errorMsg = "Unknown notification type."; + logger.error(errorMsg); + throw new Error(errorMsg); + } +} + +export default notify; diff --git a/utils/notifications/data/template.js b/src/utils/notifications/_template.ts similarity index 51% rename from utils/notifications/data/template.js rename to src/utils/notifications/_template.ts index 9a090f6..ecc327e 100644 --- a/utils/notifications/data/template.js +++ b/src/utils/notifications/_template.ts @@ -1,36 +1,41 @@ -const fs = require("fs"); -const path = require("path"); -const logger = require("../../logger"); +import fs from "fs"; +import logger from "../logger"; +const templatePath: string = "./src/data/template.json"; +const containersPath: string = "./src/data/states.json"; -const templatePath = path.join(__dirname, "template.json"); -const containersPath = path.join(__dirname, "../../../data/states.json"); +interface Template { + "text": string +} function getTemplate() { try { const data = fs.readFileSync(templatePath, "utf8"); return JSON.parse(data); - } catch (error) { - console.error("Failed to load template:", error); + } catch (error: any) { + logger.error("Failed to load template:", error); return null; } } -function setTemplate(newTemplate) { +function setTemplate(newTemplate: string) { try { fs.writeFileSync( templatePath, JSON.stringify(newTemplate, null, 2), "utf8", ); - logger.log("Template updated successfully"); - } catch (error) { + logger.debug("Template updated successfully"); + } catch (error: any) { logger.error("Failed to update template:", error); } } -function renderTemplate(containerId) { - const template = getTemplate(); - if (!template) return null; +function renderTemplate(containerId: string) { + const template: Template = getTemplate(); + if (!template) { + logger.error("Template is missing or not a string"); + return null; + } try { const data = fs.readFileSync(containersPath, "utf8"); @@ -38,12 +43,12 @@ function renderTemplate(containerId) { let containerData = null; for (const host in containers) { - containerData = containers[host].find((c) => c.id === containerId); + containerData = containers[host].find((c: any) => c.id === containerId); if (containerData) break; } if (!containerData) { - console.error(`Container with ID ${containerId} not found`); + logger.error(`Container with ID ${containerId} not found`); return null; } @@ -51,12 +56,13 @@ function renderTemplate(containerId) { return Object.keys(containerData).reduce( (text, key) => text.replace(new RegExp(`{{${key}}}`, "g"), containerData[key]), - template.message, + template.text, ); - } catch (error) { + } catch (error: any) { logger.error("Failed to load containers:", error); return null; } } -module.exports = { getTemplate, setTemplate, renderTemplate }; + +export { getTemplate, setTemplate, renderTemplate }; diff --git a/src/utils/notifications/discord.ts b/src/utils/notifications/discord.ts new file mode 100644 index 0000000..24aaf90 --- /dev/null +++ b/src/utils/notifications/discord.ts @@ -0,0 +1,55 @@ +import * as https from 'https'; +import logger from "../logger"; +import { renderTemplate } from "./_template"; + +const discord_webhook_url: string | undefined = process.env.DISCORD_WEBHOOK_URL; + +export async function discordNotification(containerId: string): Promise { + const discord_message: string | null = renderTemplate(containerId); + if (!discord_message) { + logger.error("Failed to create notification message."); + return; + } + + if (!discord_webhook_url) { + logger.error("Discord webhook URL is not set."); + return; + } + + const postData = JSON.stringify({ + content: discord_message, + }); + + const url = new URL(discord_webhook_url); + + const options = { + hostname: url.hostname, + path: url.pathname, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(postData), + }, + }; + + const req = https.request(options, (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + if (res.statusCode !== 200) { + logger.error(`Discord API error: ${data}`); + } + }); + }); + + req.on('error', (error) => { + logger.error("Error sending Discord message:", error); + }); + + req.write(postData); + req.end(); +} diff --git a/src/utils/notifications/email.ts b/src/utils/notifications/email.ts new file mode 100644 index 0000000..fbefbab --- /dev/null +++ b/src/utils/notifications/email.ts @@ -0,0 +1,46 @@ +import { SendMailOptions, createTransport } from "nodemailer"; +import logger from "../logger"; +import { renderTemplate } from "./_template"; + +const email_sender: string | undefined = process.env.EMAIL_SENDER; +const email_recipient: string | undefined = process.env.EMAIL_RECIPIENT; +const email_password: string | undefined = process.env.EMAIL_PASSWORD; +const email_service: string | undefined = process.env.EMAIL_SERVICE; + +export async function emailNotification(containerId: string) { + // Validate email configuration parameters + if (!email_sender || !email_recipient || !email_password || !email_service) { + logger.error( + "Email notification failed: Missing configuration parameters. " + + "Please ensure EMAIL_SENDER, EMAIL_RECIPIENT, EMAIL_PASSWORD, and EMAIL_SERVICE are set in environment variables.", + ); + return; + } + + const email_message: string | null = renderTemplate(containerId); + if (!email_message) { + logger.error("Failed to create notification message."); + return; + } + + const transporter = createTransport({ + service: email_service, + auth: { + user: email_sender, + pass: email_password, + }, + }); + + const mailOptions: SendMailOptions = { + from: email_sender, + to: email_recipient, + subject: "DockStat", + text: email_message, + }; + + try { + await transporter.sendMail(mailOptions); + } catch (error: any) { + logger.error("Error sending email:", error); + } +} diff --git a/src/utils/notifications/pushbullet.ts b/src/utils/notifications/pushbullet.ts new file mode 100644 index 0000000..f008e68 --- /dev/null +++ b/src/utils/notifications/pushbullet.ts @@ -0,0 +1,59 @@ +import * as https from "https"; +import logger from "../logger"; +import { renderTemplate } from "./_template"; + +const pushbullet_access_token: string | undefined = + process.env.PUSHBULLET_ACCESS_TOKEN; + +export async function pushbulletNotification( + containerId: string, +): Promise { + const pushbullet_message: string | null = renderTemplate(containerId); + if (!pushbullet_message) { + logger.error("Failed to create notification message."); + return; + } + + if (!pushbullet_access_token) { + logger.error("Pushbullet access token is not set."); + return; + } + + const postData = JSON.stringify({ + type: "note", + title: "Container Notification", + body: pushbullet_message, + }); + + const options = { + hostname: "api.pushbullet.com", + path: "/v2/pushes", + method: "POST", + headers: { + "Access-Token": pushbullet_access_token, + "Content-Type": "application/json", + "Content-Length": Buffer.byteLength(postData), + }, + }; + + const req = https.request(options, (res) => { + let data = ""; + + res.on("data", (chunk) => { + data += chunk; + }); + + res.on("end", () => { + if (res.statusCode !== 200) { + logger.error(`Pushbullet API error: ${data}`); + } + }); + }); + + req.on("error", (error) => { + logger.error("Error sending Pushbullet message:", error); + }); + + req.write(postData); + req.end(); +} diff --git a/src/utils/notifications/pushover.ts b/src/utils/notifications/pushover.ts new file mode 100644 index 0000000..847c329 --- /dev/null +++ b/src/utils/notifications/pushover.ts @@ -0,0 +1,56 @@ +import * as https from 'https'; +import logger from "../logger"; +import { renderTemplate } from "./_template"; + +const pushover_user_key: string | undefined = process.env.PUSHOVER_USER_KEY; +const pushover_api_token: string | undefined = process.env.PUSHOVER_API_TOKEN; + +export async function pushoverNotification(containerId: string): Promise { + const pushover_message: string | null = renderTemplate(containerId); + if (!pushover_message) { + logger.error("Failed to create notification message."); + return; + } + + if (!pushover_api_token || !pushover_user_key) { + logger.error("Pushover API token or user key is not set."); + return; + } + + const postData = new URLSearchParams({ + token: pushover_api_token, + user: pushover_user_key, + message: pushover_message, + }).toString(); + + const options = { + hostname: 'api.pushover.net', + path: '/1/messages.json', + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Content-Length': Buffer.byteLength(postData), + }, + }; + + const req = https.request(options, (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + if (res.statusCode !== 200) { + logger.error(`Pushover API error: ${data}`); + } + }); + }); + + req.on('error', (error) => { + logger.error("Error sending Pushover message:", error); + }); + + req.write(postData); + req.end(); +} diff --git a/src/utils/notifications/slack.ts b/src/utils/notifications/slack.ts new file mode 100644 index 0000000..b0a8e0b --- /dev/null +++ b/src/utils/notifications/slack.ts @@ -0,0 +1,55 @@ +import * as https from 'https'; +import logger from "../logger"; +import { renderTemplate } from "./_template"; + +const slack_webhook_url: string | undefined = process.env.SLACK_WEBHOOK_URL; + +export async function slackNotification(containerId: string): Promise { + const slack_message: string | null = renderTemplate(containerId); + if (!slack_message) { + logger.error("Failed to create notification message."); + return; + } + + if (!slack_webhook_url) { + logger.error("Slack webhook URL is not set."); + return; + } + + const postData = JSON.stringify({ + text: slack_message, + }); + + const url = new URL(slack_webhook_url); + + const options = { + hostname: url.hostname, + path: url.pathname, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(postData), + }, + }; + + const req = https.request(options, (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + if (res.statusCode !== 200) { + logger.error(`Slack API error: ${data}`); + } + }); + }); + + req.on('error', (error) => { + logger.error("Error sending Slack message:", error); + }); + + req.write(postData); + req.end(); +} diff --git a/src/utils/notifications/telegram.ts b/src/utils/notifications/telegram.ts new file mode 100644 index 0000000..174a12e --- /dev/null +++ b/src/utils/notifications/telegram.ts @@ -0,0 +1,55 @@ +import * as https from 'https'; +import logger from "../logger"; +import { renderTemplate } from "./_template"; + +const telegram_bot_token: string | undefined = process.env.TELEGRAM_BOT_TOKEN; +const telegram_chat_id: string | undefined = process.env.TELEGRAM_CHAT_ID; + +export async function telegramNotification(containerId: string): Promise { + const telegram_message: string | null = renderTemplate(containerId); + if (!telegram_message) { + logger.error("Failed to create notification message."); + return; + } + + if (!telegram_bot_token || !telegram_chat_id) { + logger.error("Telegram bot token or chat ID is not set."); + return; + } + + const postData = JSON.stringify({ + chat_id: telegram_chat_id, + text: telegram_message, + }); + + const options = { + hostname: 'api.telegram.org', + path: `/bot${telegram_bot_token}/sendMessage`, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(postData), + }, + }; + + const req = https.request(options, (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + if (res.statusCode !== 200) { + logger.error(`Telegram API error: ${data}`); + } + }); + }); + + req.on('error', (error) => { + logger.error("Error sending message:", error); + }); + + req.write(postData); + req.end(); +} diff --git a/src/utils/notifications/whatsapp.ts b/src/utils/notifications/whatsapp.ts new file mode 100644 index 0000000..178f6d5 --- /dev/null +++ b/src/utils/notifications/whatsapp.ts @@ -0,0 +1,57 @@ +import * as https from 'https'; +import logger from "../logger"; +import { renderTemplate } from "./_template"; + +const whatsapp_api_url: string | undefined = process.env.WHATSAPP_API_URL; +const whatsapp_recipient: string | undefined = process.env.WHATSAPP_RECIPIENT; + +export async function whatsappNotification(containerId: string): Promise { + const whatsapp_message: string | null = renderTemplate(containerId); + if (!whatsapp_message) { + logger.error("Failed to create notification message."); + return; + } + + if (!whatsapp_api_url || !whatsapp_recipient) { + logger.error("WhatsApp API URL or recipient is not set."); + return; + } + + const postData = JSON.stringify({ + to: whatsapp_recipient, + body: whatsapp_message, + }); + + const url = new URL(whatsapp_api_url); + + const options = { + hostname: url.hostname, + path: url.pathname, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(postData), + }, + }; + + const req = https.request(options, (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + if (res.statusCode !== 200) { + logger.error(`WhatsApp API error: ${data}`); + } + }); + }); + + req.on('error', (error) => { + logger.error("Error sending WhatsApp message:", error); + }); + + req.write(postData); + req.end(); +} diff --git a/src/utils/removeUnusedDeps.sh b/src/utils/removeUnusedDeps.sh new file mode 100755 index 0000000..b5b68eb --- /dev/null +++ b/src/utils/removeUnusedDeps.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +echo "Creating unused dependency list" + +TMP="$(npx depcheck --ignores @types/supports-color,ipaddr.js,dependency-cruiser,tsx,@types/bcrypt,@types/express,@types/express-handlebars,@types/node,ts-node --quiet --oneline | tail -n 1 | tr -d '\n')" + +lines=$(echo "$TMP" | tr -s ' ' '\n' | wc -l) + +if ((lines == 0)); then + echo "No unused dependencies." +else + echo + echo "Removing these unused dependencies:" + for entry in $TMP; do + echo "$entry" + done + echo +fi + + +read -n 1 -p "Delete unused dependencies? (y/n) " input +echo + +case $input in + Y|y) + COMMAND=$(echo "npm remove $TMP") + $COMMAND + exit 0 + ;; + *) + echo "Aborting" + exit 1 + ;; +esac + +exit 2 diff --git a/src/utils/swaggerDocs.ts b/src/utils/swaggerDocs.ts new file mode 100644 index 0000000..9a386dd --- /dev/null +++ b/src/utils/swaggerDocs.ts @@ -0,0 +1,11 @@ +import swaggerUi from "swagger-ui-express"; +import swaggerJsdoc from "swagger-jsdoc"; +import swaggerConfig from "../config/swaggerConfig"; +import { Express } from "express"; + +const swaggerDocs = (app: Express) => { + const specs = swaggerJsdoc(swaggerConfig); + app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(specs)); +}; + +export default swaggerDocs; diff --git a/src/utils/writeOfflineLog.ts b/src/utils/writeOfflineLog.ts new file mode 100644 index 0000000..244f62e --- /dev/null +++ b/src/utils/writeOfflineLog.ts @@ -0,0 +1,26 @@ +import fs from "fs"; +import logger from "../utils/logger"; + +const LOG_FILE_PATH = "./logs/hostStats.json"; + +async function writeOfflineLog(message: string) { + try { + if (!fs.existsSync(LOG_FILE_PATH)) { + await fs.promises.writeFile(LOG_FILE_PATH, message); + } + } catch (error: any) { + logger.error("Error writing one time reference log: ", error); + } +} + +async function readOfflineLog() { + try { + const data = await fs.promises.readFile(LOG_FILE_PATH, "utf-8"); + logger.debug("Returning data:", data); + return data; + } catch (error: any) { + logger.error("Error reading offline log:", error); + } +} + +export { writeOfflineLog, readOfflineLog }; diff --git a/swagger/swaggerDocs.js b/swagger/swaggerDocs.js deleted file mode 100644 index 5719372..0000000 --- a/swagger/swaggerDocs.js +++ /dev/null @@ -1,10 +0,0 @@ -const swaggerUi = require("swagger-ui-express"); -const swaggerJsdoc = require("swagger-jsdoc"); -const swaggerConfig = require("../config/swaggerConfig"); - -const swaggerDocs = (app) => { - const specs = swaggerJsdoc(swaggerConfig); - app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(specs)); -}; - -module.exports = swaggerDocs; diff --git a/tests/main.spec.ts b/tests/main.spec.ts new file mode 100644 index 0000000..f900642 --- /dev/null +++ b/tests/main.spec.ts @@ -0,0 +1,131 @@ +import { test, expect } from '@playwright/test'; +import ora from 'ora'; + +interface Route { + url: string; +} + +interface FrontendRoute { + url: string; + type: string; +} + +const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + +test('Swagger - Auth enable and disable', async ({ page }) => { + await page.goto('http://localhost:9876/api-docs/'); + await page.getByLabel('post /auth/enable').click(); + await page.getByRole('button', { name: 'Try it out' }).click(); + await page.getByPlaceholder('password').click(); + await page.getByPlaceholder('password').fill('1'); + await page.getByRole('button', { name: 'Execute' }).click(); + await page.getByRole('button', { name: 'Authorize' }).click(); + await page.getByLabel('Value:').click(); + await page.getByLabel('Value:').fill('1'); + await page.getByLabel('Apply credentials').click(); + await page.getByRole('button', { name: 'Close' }).click(); + await page.getByLabel('post /auth/disable').click(); + await page.getByRole('button', { name: 'Try it out' }).click(); + await page.getByRole('row', { name: 'password *required (query)', exact: true }).getByPlaceholder('password').click(); + await page.getByRole('row', { name: 'password *required (query)', exact: true }).getByPlaceholder('password').fill('1'); + await page.locator('#operations-Authentication-post_auth_disable').getByRole('button', { name: 'Execute' }).click(); +}); + +test('Return 200 status code', async ({ request }) => { + await sleep(5000); + const getRoutes: Route[] = [ + { url: 'http://localhost:9876/data/latest' }, + { url: 'http://localhost:9876/data/time/24h' }, + { url: 'http://localhost:9876/api/hosts' }, + { url: 'http://localhost:9876/api/host/Fin-2/stats' }, + { url: 'http://localhost:9876/api/containers' }, + { url: 'http://localhost:9876/api/config' }, + { url: 'http://localhost:9876/api/current-schedule' }, + { url: 'http://localhost:9876/api/frontend-config' }, + { url: 'http://localhost:9876/api/status' }, + { url: 'http://localhost:9876/ha/config' }, + { url: 'http://localhost:9876/ha/prepare-sync' }, + { url: 'http://localhost:9876/notification-service/get-template' } + ]; + + for (const { url } of getRoutes) { + const spinner = ora(`Checking: ${url}`).start(); + const response = await request.get(`${url}`); + await sleep(1000); + if (response.status() === 200) { + spinner.succeed(`Checked: ${url}`); + } else { + spinner.fail(`Failed: ${url}`); + } + expect(response.status()).toBe(200); + } + + const putRoutes: Route[] = [ + { url: 'http://localhost:9876/conf/addHost?name=test&url=localhost&port=2375' }, + { url: 'http://localhost:9876/conf/scheduler?interval=300s' } + ]; + + for (const { url } of putRoutes) { + const spinner = ora(`Checking: ${url}`).start(); + const response = await request.put(`${url}`); + await sleep(1000); + if (response.status() === 200) { + spinner.succeed(`Checked: ${url}`); + } else { + spinner.fail(`Failed: ${url}`); + } + expect(response.status()).toBe(200); + } + + const data = { text: "{{name}} ({{id}}) on {{hostName}} is {{state}}." }; + + const spinner = ora('Checking: http://localhost:9876/notification-service/set-template').start(); + const response = await request.post('http://localhost:9876/notification-service/set-template', { data }); + await sleep(1000); + if (response.status() === 200) { + spinner.succeed('Checked: http://localhost:9876/notification-service/set-template'); + } else { + spinner.fail('Failed: http://localhost:9876/notification-service/set-template'); + } + expect(response.status()).toBe(200); + + // Remove test host: + const deleteSpinner = ora('Removing test host').start(); + await request.delete('http://localhost:9876/conf/removeHost?hostName=test'); + await sleep(1000); + deleteSpinner.succeed('Removed test host'); + + const frontendRoutes: FrontendRoute[] = [ + { url: 'http://localhost:9876/frontend/tag/test/test', type: "post" }, + { url: 'http://localhost:9876/frontend/pin/test', type: "post" }, + { url: 'http://localhost:9876/frontend/add-link/test/https%3A%2F%2Fexample.com', type: "post" }, + { url: 'http://localhost:9876/frontend/add-icon/test/test.png/true', type: "post" }, + { url: 'http://localhost:9876/frontend/hide/test', type: "delete" }, + { url: 'http://localhost:9876/frontend/remove-tag/test/test', type: "delete" }, + { url: 'http://localhost:9876/frontend/remove-link/test', type: "delete" }, + { url: 'http://localhost:9876/frontend/show/test', type: "post" }, + { url: 'http://localhost:9876/frontend/remove-icon/test', type: "delete" }, + { url: 'http://localhost:9876/frontend/unpin/test', type: "delete" } + ]; + + for (const { url, type } of frontendRoutes) { + const spinner = ora(`Checking: ${url}`).start(); + let response; + if (type === "post") { + response = await request.post(`${url}`); + } else if (type === "put") { + response = await request.put(`${url}`); + } else if (type === "delete") { + response = await request.delete(`${url}`); + } else { + throw new Error(`Unsupported request type: ${type}`); + } + await sleep(1000); + if (response.status() === 200) { + spinner.succeed(`Checked: ${url}`); + } else { + spinner.fail(`Failed: ${url}`); + } + expect(response.status()).toBe(200); + } +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..8fc3c32 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "outDir": "dist/src", + "module": "CommonJS", + "moduleResolution": "node", + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "esModuleInterop": true + }, + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Recommended", + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "**/*.spec.ts" + ] +} \ No newline at end of file diff --git a/utils/containerService.js b/utils/containerService.js deleted file mode 100644 index eb078a5..0000000 --- a/utils/containerService.js +++ /dev/null @@ -1,63 +0,0 @@ -const config = require("../config/dockerConfig.json"); -const logger = require("./logger"); -const { getDockerClient } = require("./dockerClient"); - -async function fetchAllContainers() { - const allContainerData = {}; - - for (const hostConfig of config.hosts) { - const hostName = hostConfig.name; - try { - const docker = getDockerClient(hostName); - const containers = await docker.listContainers({ all: true }); - - allContainerData[hostName] = await Promise.all( - containers.map(async (container) => { - const containerInfo = await docker - .getContainer(container.Id) - .inspect(); - const containerStats = await docker - .getContainer(container.Id) - .stats({ stream: false }); - const cpuDelta = - containerStats.cpu_stats.cpu_usage.total_usage - - containerStats.precpu_stats.cpu_usage.total_usage; - const systemCpuDelta = - containerStats.cpu_stats.system_cpu_usage - - containerStats.precpu_stats.system_cpu_usage; - const cpuUsage = - systemCpuDelta > 0 - ? (cpuDelta / systemCpuDelta) * - containerStats.cpu_stats.online_cpus - : 0; - - return { - name: container.Names[0].replace("/", ""), - id: container.Id, - hostName: hostName, - state: container.State, - cpu_usage: cpuUsage * 1000000000, - mem_usage: containerStats.memory_stats.usage, - mem_limit: containerStats.memory_stats.limit, - net_rx: containerStats.networks?.eth0?.rx_bytes || 0, - net_tx: containerStats.networks?.eth0?.tx_bytes || 0, - current_net_rx: containerStats.networks?.eth0?.rx_bytes || 0, - current_net_tx: containerStats.networks?.eth0?.tx_bytes || 0, - networkMode: containerInfo.HostConfig.NetworkMode, - }; - }), - ); - } catch (error) { - logger.error( - `Error fetching containers for host: ${hostName} - ${error.message}`, - ); - allContainerData[hostName] = { - error: `Error fetching containers: ${error.message}`, - }; - } - } - - return allContainerData; -} - -module.exports = { fetchAllContainers }; diff --git a/utils/createDependencyGraph.sh b/utils/createDependencyGraph.sh deleted file mode 100755 index 3e75de0..0000000 --- a/utils/createDependencyGraph.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/bash - -TMP=$(mktemp) - -cat ./server.js | grep "./routes" | awk '{print $2,$4}' > $TMP - -while read line; do - target_route=$(echo "$line" | cut -d '"' -f2) - route=$(echo "$line" | awk '{print $1}') - - echo - echo "Route: $route" - echo ${target_route}.js - - - npx depcruise \ - -p cli-feedback \ - -T mermaid \ - -x "^node_modules|logger|.dependency-cruiser|path|fs" \ - -f ./misc/dependencyGraphs/mermaid-${route}.txt \ - ${target_route}.js - -done < <(cat $TMP) - -npx depcruise \ - -p cli-feedback \ - -T mermaid \ - -x "^node_modules|logger|.dependency-cruiser|path|fs" \ - -f ./misc/dependencyGraphs/mermaid-all.txt \ - ./ - -sleep 0.5 - -echo -e "\n========\n\n DONE\n\n========" diff --git a/utils/dockerClient.js b/utils/dockerClient.js deleted file mode 100644 index 3d691e5..0000000 --- a/utils/dockerClient.js +++ /dev/null @@ -1,45 +0,0 @@ -const Docker = require("dockerode"); -const fs = require("fs"); -const path = require("path"); -const logger = require("./logger"); - -// Function to dynamically load config on each request -function loadDockerConfig() { - const configPath = path.join(__dirname, "../config/dockerConfig.json"); - try { - const rawData = fs.readFileSync(configPath); - logger.debug("Refreshed DockerConfig.json"); - return JSON.parse(rawData); - } catch (error) { - logger.error("Error loading dockerConfig.json: " + error.message); - throw new Error("Failed to load Docker configuration"); - } -} - -// Function to create the Docker client using separate url and port -function createDockerClient(hostConfig) { - logger.info( - `Creating Docker client for host: ${hostConfig.url} on port: ${hostConfig.port}`, - ); - return new Docker({ - host: hostConfig.url, - port: hostConfig.port || 2375, // Use 2375 as default port for non-TLS - protocol: "http", // Ensure the use of http for non-TLS - }); -} - -// This function will get the Docker client based on the host configuration -const getDockerClient = (hostName) => { - logger.debug(`Getting Docker Client for ${hostName}`); - const config = loadDockerConfig(); // Dynamically load config - const hostConfig = config.hosts.find((host) => host.name === hostName); - - if (!hostConfig) { - const errorMsg = `Docker host ${hostName} not found in configuration`; - logger.error(errorMsg); - throw new Error(errorMsg); - } - return createDockerClient(hostConfig); -}; - -module.exports = { getDockerClient }; diff --git a/utils/extractHostData.js b/utils/extractHostData.js deleted file mode 100644 index 87db239..0000000 --- a/utils/extractHostData.js +++ /dev/null @@ -1,26 +0,0 @@ -function extractRelevantData(jsonData) { - return { - hostName: jsonData.hostName, - info: { - ID: jsonData.info.ID, - Containers: jsonData.info.Containers, - ContainersRunning: jsonData.info.ContainersRunning, - ContainersPaused: jsonData.info.ContainersPaused, - ContainersStopped: jsonData.info.ContainersStopped, - Images: jsonData.info.Images, - OperatingSystem: jsonData.info.OperatingSystem, - KernelVersion: jsonData.info.KernelVersion, - Architecture: jsonData.info.Architecture, - MemTotal: jsonData.info.MemTotal, - NCPU: jsonData.info.NCPU, - }, - version: { - Components: jsonData.version.Components.reduce((acc, component) => { - acc[component.Name] = component.Version; - return acc; - }, {}), - }, - }; -} - -module.exports = extractRelevantData; diff --git a/utils/notifications/_notify.js b/utils/notifications/_notify.js deleted file mode 100644 index b4a96fd..0000000 --- a/utils/notifications/_notify.js +++ /dev/null @@ -1,59 +0,0 @@ -const logger = require("../../utils/logger"); - -const { telegramNotification } = require("./telegram"); -const { slackNotification } = require("./slack"); -const { discordNotification } = require("./discord"); -const { emailNotification } = require("./email"); -const { whatsappNotification } = require("./whatsapp"); -const { pushbulletNotification } = require("./pushbullet"); -const { pushoverNotification } = require("./pushover"); - -async function notify(type, containerId) { - if (!containerId) { - logger.error("Container ID is required."); - throw new Error("Container ID is required."); - } - - switch (type) { - case "telegram": - logger.debug("Testing Telegram notification..."); - await telegramNotification(containerId); - break; - case "slack": - logger.debug("Testing Slack notification..."); - await slackNotification(containerId); - break; - case "discord": - logger.debug("Testing Discord notification..."); - await discordNotification(containerId); - break; - case "email": - logger.debug("Testing Email notification..."); - await emailNotification(containerId); - break; - case "whatsapp": - logger.debug("Testing WhatsApp notification..."); - await whatsappNotification(containerId); - break; - case "pushbullet": - logger.debug("Testing Pushbullet notification..."); - await pushbulletNotification(containerId); - break; - case "pushover": - logger.debug("Testing Pushover notification..."); - await pushoverNotification(containerId); - break; - default: - const errorMsg = "Unknown notification type."; - logger.error(errorMsg); - throw new Error(errorMsg); - } -} - -if (require.main === module) { - const [type, containerId] = process.argv.slice(2); - notify(type, containerId); - console.log(`Testing ${type}, with: ${containerId}`); -} - -module.exports = notify; diff --git a/utils/notifications/data/template.json b/utils/notifications/data/template.json deleted file mode 100644 index 6a57d44..0000000 --- a/utils/notifications/data/template.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "text": "{{name}} ({{id}}) on {{host}} is {{state}}." -} \ No newline at end of file diff --git a/utils/notifications/discord.js b/utils/notifications/discord.js deleted file mode 100644 index c7bfe82..0000000 --- a/utils/notifications/discord.js +++ /dev/null @@ -1,27 +0,0 @@ -import fetch from "node-fetch"; -import logger from "../logger.js"; -import { renderTemplate } from "./data/template.js"; - -const discord_webhook_url = process.env.DISCORD_WEBHOOK_URL; - -export async function discordNotification(containerId) { - const discord_message = renderTemplate(containerId); - if (!discord_message) { - logger.error("Failed to create notification message."); - return; - } - - try { - await fetch(discord_webhook_url, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - content: discord_message, - }), - }); - } catch (error) { - logger.error("Error sending Discord message:", error); - } -} diff --git a/utils/notifications/email.js b/utils/notifications/email.js deleted file mode 100644 index d701679..0000000 --- a/utils/notifications/email.js +++ /dev/null @@ -1,36 +0,0 @@ -import nodemailer from "nodemailer"; -import logger from "../logger.js"; -import { renderTemplate } from "./data/template.js"; - -const email_sender = process.env.EMAIL_SENDER; -const email_recipient = process.env.EMAIL_RECIPIENT; -const email_password = process.env.EMAIL_PASSWORD; - -export async function emailNotification(containerId) { - const email_message = renderTemplate(containerId); - if (!email_message) { - logger.error("Failed to create notification message."); - return; - } - - const transporter = nodemailer.createTransport({ - service: "gmail", - auth: { - user: email_sender, - pass: email_password, - }, - }); - - const mailOptions = { - from: email_sender, - to: email_recipient, - subject: "Container Notification", - text: email_message, - }; - - try { - await transporter.sendMail(mailOptions); - } catch (error) { - logger.error("Error sending email:", error); - } -} diff --git a/utils/notifications/pushbullet.js b/utils/notifications/pushbullet.js deleted file mode 100644 index 442f44d..0000000 --- a/utils/notifications/pushbullet.js +++ /dev/null @@ -1,30 +0,0 @@ -import fetch from "node-fetch"; -import logger from "../logger.js"; -import { renderTemplate } from "./data/template.js"; - -const pushbullet_access_token = process.env.PUSHBULLET_ACCESS_TOKEN; - -export async function pushbulletNotification(containerId) { - const pushbullet_message = renderTemplate(containerId); - if (!pushbullet_message) { - logger.error("Failed to create notification message."); - return; - } - - try { - await fetch("https://api.pushbullet.com/v2/pushes", { - method: "POST", - headers: { - "Access-Token": pushbullet_access_token, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - type: "note", - title: "Container Notification", - body: pushbullet_message, - }), - }); - } catch (error) { - logger.error("Error sending Pushbullet message:", error); - } -} diff --git a/utils/notifications/pushover.js b/utils/notifications/pushover.js deleted file mode 100644 index 592e7f0..0000000 --- a/utils/notifications/pushover.js +++ /dev/null @@ -1,30 +0,0 @@ -import fetch from "node-fetch"; -import logger from "../logger.js"; -import { renderTemplate } from "./data/template.js"; - -const pushover_user_key = process.env.PUSHOVER_USER_KEY; -const pushover_api_token = process.env.PUSHOVER_API_TOKEN; - -export async function pushoverNotification(containerId) { - const pushover_message = renderTemplate(containerId); - if (!pushover_message) { - logger.error("Failed to create notification message."); - return; - } - - try { - await fetch("https://api.pushover.net/1/messages.json", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: new URLSearchParams({ - token: pushover_api_token, - user: pushover_user_key, - message: pushover_message, - }), - }); - } catch (error) { - logger.error("Error sending Pushover message:", error); - } -} diff --git a/utils/notifications/slack.js b/utils/notifications/slack.js deleted file mode 100644 index 2c1a67a..0000000 --- a/utils/notifications/slack.js +++ /dev/null @@ -1,27 +0,0 @@ -import fetch from "node-fetch"; -import logger from "../logger.js"; -import { renderTemplate } from "./data/template.js"; - -const slack_webhook_url = process.env.SLACK_WEBHOOK_URL; - -export async function slackNotification(containerId) { - const slack_message = renderTemplate(containerId); - if (!slack_message) { - logger.error("Failed to create notification message."); - return; - } - - try { - await fetch(slack_webhook_url, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - text: slack_message, - }), - }); - } catch (error) { - logger.error("Error sending Slack message:", error); - } -} diff --git a/utils/notifications/telegram.js b/utils/notifications/telegram.js deleted file mode 100644 index 5c79bdc..0000000 --- a/utils/notifications/telegram.js +++ /dev/null @@ -1,32 +0,0 @@ -import fetch from "node-fetch"; -import logger from "../logger.js"; -import { renderTemplate } from "./data/template.js"; - -const telegram_bot_token = process.env.TELEGRAM_BOT_TOKEN; -const telegram_chat_id = process.env.TELEGRAM_CHAT_ID; - -export async function telegramNotification(containerId) { - const telegram_message = renderTemplate(containerId); - if (!telegram_message) { - logger.error("Failed to create notification message."); - return; - } - - try { - await fetch( - `https://api.telegram.org/bot${telegram_bot_token}/sendMessage`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - chat_id: telegram_chat_id, - text: telegram_message, - }), - }, - ); - } catch (error) { - logger.error("Error sending message:", error); - } -} diff --git a/utils/notifications/whatsapp.js b/utils/notifications/whatsapp.js deleted file mode 100644 index d714b0b..0000000 --- a/utils/notifications/whatsapp.js +++ /dev/null @@ -1,29 +0,0 @@ -import fetch from "node-fetch"; -import logger from "../logger.js"; -import { renderTemplate } from "./data/template.js"; - -const whatsapp_api_url = process.env.WHATSAPP_API_URL; // e.g., Twilio or other API service -const whatsapp_recipient = process.env.WHATSAPP_RECIPIENT; - -export async function whatsappNotification(containerId) { - const whatsapp_message = renderTemplate(containerId); - if (!whatsapp_message) { - logger.error("Failed to create notification message."); - return; - } - - try { - await fetch(whatsapp_api_url, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - to: whatsapp_recipient, - body: whatsapp_message, - }), - }); - } catch (error) { - logger.error("Error sending WhatsApp message:", error); - } -} diff --git a/utils/writeOfflineLog.js b/utils/writeOfflineLog.js deleted file mode 100644 index 4d26b1d..0000000 --- a/utils/writeOfflineLog.js +++ /dev/null @@ -1,31 +0,0 @@ -const fs = require("fs"); -const path = require("path"); -const logger = require("../utils/logger"); - -const LOG_FILE_PATH = path.join(__dirname, "../logs/hostStats.json"); - -function writeOfflineLog(message) { - try { - if (!fs.existsSync(LOG_FILE_PATH)) { - fs.writeFileSync(LOG_FILE_PATH, message); - } - } catch (error) { - logger.error("Error writing one time reference log: ", error); - } -} - -function readOfflineLog() { - fs.readFile(LOG_FILE_PATH, "utf-8", (err, data) => { - if (err) { - logger.error("Error reading offline log:", err); - } - - logger.debug("Returning data:", data); - return data; - }); -} - -module.exports = { - writeOfflineLog, - readOfflineLog, -}; diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..1418f00 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,3298 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@apidevtools/json-schema-ref-parser@^9.0.6": + version "9.1.2" + resolved "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz" + integrity sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg== + dependencies: + "@jsdevtools/ono" "^7.1.3" + "@types/json-schema" "^7.0.6" + call-me-maybe "^1.0.1" + js-yaml "^4.1.0" + +"@apidevtools/openapi-schemas@^2.0.4": + version "2.1.0" + resolved "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz" + integrity sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ== + +"@apidevtools/swagger-methods@^3.0.2": + version "3.0.2" + resolved "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz" + integrity sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg== + +"@apidevtools/swagger-parser@10.0.3": + version "10.0.3" + resolved "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz" + integrity sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g== + dependencies: + "@apidevtools/json-schema-ref-parser" "^9.0.6" + "@apidevtools/openapi-schemas" "^2.0.4" + "@apidevtools/swagger-methods" "^3.0.2" + "@jsdevtools/ono" "^7.1.3" + call-me-maybe "^1.0.1" + z-schema "^5.0.1" + +"@babel/generator@7.18.2": + version "7.18.2" + resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.18.2.tgz" + integrity sha512-W1lG5vUwFvfMd8HVXqdfbuG7RuaSrTCCD8cl8fP8wOivdbtbIg2Db3IWUcgvfxKbbn6ZBGYRW/Zk1MIwK49mgw== + dependencies: + "@babel/types" "^7.18.2" + "@jridgewell/gen-mapping" "^0.3.0" + jsesc "^2.5.1" + +"@babel/helper-string-parser@^7.18.10": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz" + integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== + +"@babel/helper-validator-identifier@^7.18.6": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz" + integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== + +"@babel/parser@7.18.4": + version "7.18.4" + resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.18.4.tgz" + integrity sha512-FDge0dFazETFcxGw/EXzOkN8uJp0PC7Qbm+Pe9T+av2zlBpOgunFHkQPPn+eRuClU73JF+98D531UgayY89tow== + +"@babel/types@^7.18.2", "@babel/types@7.19.0": + version "7.19.0" + resolved "https://registry.npmjs.org/@babel/types/-/types-7.19.0.tgz" + integrity sha512-YuGopBq3ke25BVSiS6fgF49Ul9gH1x70Bcr6bqRLjWCkcX8Hre1/5+z+IiWOIerRMSSEfGZVB9z9kyq7wVs9YA== + dependencies: + "@babel/helper-string-parser" "^7.18.10" + "@babel/helper-validator-identifier" "^7.18.6" + to-fast-properties "^2.0.0" + +"@balena/dockerignore@^1.0.2": + version "1.0.2" + resolved "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz" + integrity sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q== + +"@colors/colors@^1.6.0", "@colors/colors@1.6.0": + version "1.6.0" + resolved "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz" + integrity sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA== + +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + +"@dabh/diagnostics@^2.0.2": + version "2.0.3" + resolved "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz" + integrity sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA== + dependencies: + colorspace "1.1.x" + enabled "2.0.x" + kuler "^2.0.0" + +"@esbuild/linux-x64@0.23.1": + version "0.23.1" + resolved "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz" + integrity sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ== + +"@gar/promisify@^1.0.1": + version "1.1.3" + resolved "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz" + integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== + +"@jridgewell/gen-mapping@^0.3.0": + version "0.3.5" + resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz" + integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== + dependencies: + "@jridgewell/set-array" "^1.2.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": + version "1.5.0" + resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== + +"@jridgewell/trace-mapping@^0.3.24": + version "0.3.25" + resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@jsdevtools/ono@^7.1.3": + version "7.1.3" + resolved "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz" + integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg== + +"@mapbox/node-pre-gyp@^1.0.11": + version "1.0.11" + resolved "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz" + integrity sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ== + dependencies: + detect-libc "^2.0.0" + https-proxy-agent "^5.0.0" + make-dir "^3.1.0" + node-fetch "^2.6.7" + nopt "^5.0.0" + npmlog "^5.0.1" + rimraf "^3.0.2" + semver "^7.3.5" + tar "^6.1.11" + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5": + version "2.0.5" + resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@npmcli/fs@^1.0.0": + version "1.1.1" + resolved "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz" + integrity sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ== + dependencies: + "@gar/promisify" "^1.0.1" + semver "^7.3.5" + +"@npmcli/move-file@^1.0.1": + version "1.1.2" + resolved "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz" + integrity sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg== + dependencies: + mkdirp "^1.0.4" + rimraf "^3.0.2" + +"@playwright/test@^1.49.0": + version "1.49.0" + resolved "https://registry.npmjs.org/@playwright/test/-/test-1.49.0.tgz" + integrity sha512-DMulbwQURa8rNIQrf94+jPJQ4FmOVdpE5ZppRNvWVjvhC+6sOeo28r8MgIpQRYouXRtt/FCCXU7zn20jnHR4Qw== + dependencies: + playwright "1.49.0" + +"@scarf/scarf@=1.4.0": + version "1.4.0" + resolved "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz" + integrity sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ== + +"@tootallnate/once@1": + version "1.1.2" + resolved "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz" + integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== + +"@tsconfig/node10@^1.0.7": + version "1.0.11" + resolved "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz" + integrity sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.4" + resolved "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz" + integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== + +"@types/bcrypt@^5.0.2": + version "5.0.2" + resolved "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz" + integrity sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ== + dependencies: + "@types/node" "*" + +"@types/body-parser@*": + version "1.19.5" + resolved "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz" + integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.38" + resolved "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz" + integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== + dependencies: + "@types/node" "*" + +"@types/cors@^2.8.17": + version "2.8.17" + resolved "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz" + integrity sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA== + dependencies: + "@types/node" "*" + +"@types/docker-modem@*": + version "3.0.6" + resolved "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz" + integrity sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg== + dependencies: + "@types/node" "*" + "@types/ssh2" "*" + +"@types/dockerode@^3.3.31": + version "3.3.32" + resolved "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.32.tgz" + integrity sha512-xxcG0g5AWKtNyh7I7wswLdFvym4Mlqks5ZlKzxEUrGHS0r0PUOfxm2T0mspwu10mHQqu3Ck3MI3V2HqvLWE1fg== + dependencies: + "@types/docker-modem" "*" + "@types/node" "*" + "@types/ssh2" "*" + +"@types/express-handlebars@^5.3.1": + version "5.3.1" + resolved "https://registry.npmjs.org/@types/express-handlebars/-/express-handlebars-5.3.1.tgz" + integrity sha512-DSzaERLO4gHb8AqnrL58jzSDyT0yDdl6HqDc+bGz1Hf0nrG1FK30nHGzv8NBEGR8QV9eUGB/YaE0Qj3NjF7siw== + +"@types/express-serve-static-core@^5.0.0": + version "5.0.2" + resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.2.tgz" + integrity sha512-vluaspfvWEtE4vcSDlKRNer52DvOGrB2xv6diXy6UKyKW0lqZiWHGNApSyxOv+8DE5Z27IzVvE7hNkxg7EXIcg== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express@*", "@types/express@^5.0.0": + version "5.0.0" + resolved "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz" + integrity sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^5.0.0" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/http-errors@*": + version "2.0.4" + resolved "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz" + integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== + +"@types/json-schema@^7.0.6": + version "7.0.15" + resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +"@types/mime@^1": + version "1.3.5" + resolved "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz" + integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== + +"@types/node-fetch@^2.6.12": + version "2.6.12" + resolved "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz" + integrity sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA== + dependencies: + "@types/node" "*" + form-data "^4.0.0" + +"@types/node@*", "@types/node@^22.9.0": + version "22.10.1" + resolved "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz" + integrity sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ== + dependencies: + undici-types "~6.20.0" + +"@types/node@^18.11.18": + version "18.19.67" + resolved "https://registry.npmjs.org/@types/node/-/node-18.19.67.tgz" + integrity sha512-wI8uHusga+0ZugNp0Ol/3BqQfEcCCNfojtO6Oou9iVNGPTL6QNSdnUdqq85fRgIorLhLMuPIKpsN98QE9Nh+KQ== + dependencies: + undici-types "~5.26.4" + +"@types/nodemailer@^6.4.17": + version "6.4.17" + resolved "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz" + integrity sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww== + dependencies: + "@types/node" "*" + +"@types/qs@*": + version "6.9.17" + resolved "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz" + integrity sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ== + +"@types/range-parser@*": + version "1.2.7" + resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz" + integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== + +"@types/send@*": + version "0.17.4" + resolved "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz" + integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/serve-static@*": + version "1.15.7" + resolved "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz" + integrity sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw== + dependencies: + "@types/http-errors" "*" + "@types/node" "*" + "@types/send" "*" + +"@types/ssh2@*": + version "1.15.1" + resolved "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.1.tgz" + integrity sha512-ZIbEqKAsi5gj35y4P4vkJYly642wIbY6PqoN0xiyQGshKUGXR9WQjF/iF9mXBQ8uBKy3ezfsCkcoHKhd0BzuDA== + dependencies: + "@types/node" "^18.11.18" + +"@types/supports-color@^8.1.3": + version "8.1.3" + resolved "https://registry.npmjs.org/@types/supports-color/-/supports-color-8.1.3.tgz" + integrity sha512-Hy6UMpxhE3j1tLpl27exp1XqHD7n8chAiNPzWfz16LPZoMMoSc4dzLl6w9qijkEb/r5O1ozdu1CWGA2L83ZeZg== + +"@types/swagger-jsdoc@^6.0.4": + version "6.0.4" + resolved "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.4.tgz" + integrity sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ== + +"@types/swagger-ui-express@^4.1.7": + version "4.1.7" + resolved "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.7.tgz" + integrity sha512-ovLM9dNincXkzH4YwyYpll75vhzPBlWx6La89wwvYH7mHjVpf0X0K/vR/aUM7SRxmr5tt9z7E5XJcjQ46q+S3g== + dependencies: + "@types/express" "*" + "@types/serve-static" "*" + +"@types/triple-beam@^1.3.2": + version "1.3.5" + resolved "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz" + integrity sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw== + +abbrev@1: + version "1.1.1" + resolved "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + +accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +acorn-jsx-walk@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/acorn-jsx-walk/-/acorn-jsx-walk-2.0.0.tgz" + integrity sha512-uuo6iJj4D4ygkdzd6jPtcxs8vZgDX9YFIkqczGImoypX2fQ4dVImmu3UzA4ynixCIMTrEOWW+95M2HuBaCEOVA== + +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn-loose@^8.4.0: + version "8.4.0" + resolved "https://registry.npmjs.org/acorn-loose/-/acorn-loose-8.4.0.tgz" + integrity sha512-M0EUka6rb+QC4l9Z3T0nJEzNOO7JcoJlYMrBlyBCiFSXRyxjLKayd4TbQs2FDRWQU1h9FR7QVNHt+PEaoNL5rQ== + dependencies: + acorn "^8.11.0" + +acorn-walk@^8.1.1, acorn-walk@^8.3.4: + version "8.3.4" + resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz" + integrity sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g== + dependencies: + acorn "^8.11.0" + +"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.11.0, acorn@^8.14.0, acorn@^8.4.1: + version "8.14.0" + resolved "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz" + integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA== + +agent-base@^6.0.2, agent-base@6: + version "6.0.2" + resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + +agentkeepalive@^4.1.3: + version "4.5.0" + resolved "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz" + integrity sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew== + dependencies: + humanize-ms "^1.2.1" + +aggregate-error@^3.0.0: + version "3.1.0" + resolved "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" + +ajv@^8.17.1: + version "8.17.1" + resolved "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz" + integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== + dependencies: + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-regex@^6.0.1: + version "6.1.0" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz" + integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +"aproba@^1.0.3 || ^2.0.0": + version "2.0.0" + resolved "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz" + integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== + +are-we-there-yet@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz" + integrity sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw== + dependencies: + delegates "^1.0.0" + readable-stream "^3.6.0" + +are-we-there-yet@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz" + integrity sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg== + dependencies: + delegates "^1.0.0" + readable-stream "^3.6.0" + +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +asn1@^0.2.6: + version "0.2.6" + resolved "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz" + integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== + dependencies: + safer-buffer "~2.1.0" + +async@^3.2.3: + version "3.2.6" + resolved "https://registry.npmjs.org/async/-/async-3.2.6.tgz" + integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +at-least-node@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz" + integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +bcrypt-pbkdf@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz" + integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w== + dependencies: + tweetnacl "^0.14.3" + +bcrypt@^5.1.1: + version "5.1.1" + resolved "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz" + integrity sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww== + dependencies: + "@mapbox/node-pre-gyp" "^1.0.11" + node-addon-api "^5.0.0" + +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + +bindings@^1.5.0: + version "1.5.0" + resolved "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz" + integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== + dependencies: + file-uri-to-path "1.0.0" + +bl@^4.0.3: + version "4.1.0" + resolved "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + +body-parser@1.20.3: + version "1.20.3" + resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz" + integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== + 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.13.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^3.0.3, braces@~3.0.2: + version "3.0.3" + resolved "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + +buildcheck@~0.0.6: + version "0.0.6" + resolved "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz" + integrity sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A== + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +cacache@^15.2.0: + version "15.3.0" + resolved "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz" + integrity sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ== + dependencies: + "@npmcli/fs" "^1.0.0" + "@npmcli/move-file" "^1.0.1" + chownr "^2.0.0" + fs-minipass "^2.0.0" + glob "^7.1.4" + infer-owner "^1.0.4" + lru-cache "^6.0.0" + minipass "^3.1.1" + minipass-collect "^1.0.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.2" + mkdirp "^1.0.3" + p-map "^4.0.0" + promise-inflight "^1.0.1" + rimraf "^3.0.2" + ssri "^8.0.1" + tar "^6.0.2" + unique-filename "^1.1.1" + +call-bind-apply-helpers@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz" + integrity sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + +call-bind@^1.0.7: + version "1.0.8" + resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz" + integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== + dependencies: + call-bind-apply-helpers "^1.0.0" + es-define-property "^1.0.0" + get-intrinsic "^1.2.4" + set-function-length "^1.2.2" + +call-me-maybe@^1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz" + integrity sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ== + +chalk@^4.1.0: + version "4.1.2" + resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chalk@^4.1.2: + version "4.1.2" + resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chalk@^5.3.0: + version "5.3.0" + resolved "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz" + integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== + +chokidar@^3.5.2: + version "3.6.0" + resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chokidar@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz" + integrity sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA== + dependencies: + readdirp "^4.0.1" + +chownr@^1.1.1: + version "1.1.4" + resolved "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz" + integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== + +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== + +cli-cursor@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz" + integrity sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw== + dependencies: + restore-cursor "^5.0.0" + +cli-spinners@^2.9.2: + version "2.9.2" + resolved "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz" + integrity sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg== + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +color-convert@^1.9.3: + version "1.9.3" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@^1.0.0, color-name@1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +color-string@^1.6.0: + version "1.9.1" + resolved "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz" + integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + +color-support@^1.1.2, color-support@^1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz" + integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== + +color@^3.1.3: + version "3.2.1" + resolved "https://registry.npmjs.org/color/-/color-3.2.1.tgz" + integrity sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA== + dependencies: + color-convert "^1.9.3" + color-string "^1.6.0" + +colorspace@1.1.x: + version "1.1.4" + resolved "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz" + integrity sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w== + dependencies: + color "^3.1.3" + text-hex "1.0.x" + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@^12.1.0: + version "12.1.0" + resolved "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz" + integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA== + +commander@^9.4.1: + version "9.5.0" + resolved "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz" + integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== + +commander@6.2.0: + version "6.2.0" + resolved "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz" + integrity sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +console-control-strings@^1.0.0, console-control-strings@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz" + integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4, content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.7.1: + version "0.7.1" + resolved "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz" + integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +cors@^2.8.5: + version "2.8.5" + resolved "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + +cpu-features@~0.0.10: + version "0.0.10" + resolved "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz" + integrity sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA== + dependencies: + buildcheck "~0.0.6" + nan "^2.19.0" + +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + +data-uri-to-buffer@^4.0.0: + version "4.0.1" + resolved "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz" + integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A== + +debug@^4, debug@^4.1.1, debug@^4.3.3, debug@4: + version "4.4.0" + resolved "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz" + integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== + dependencies: + ms "^2.1.3" + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +decompress-response@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz" + integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== + dependencies: + mimic-response "^3.1.0" + +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + +define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz" + integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +dependency-cruiser@^16.5.0: + version "16.7.0" + resolved "https://registry.npmjs.org/dependency-cruiser/-/dependency-cruiser-16.7.0.tgz" + integrity sha512-522LLjHINl9r0RIZ8/6s6TqIHTuEJG3XDU2WPSm9dG0rvLUYVyQwE9ID31tDFs4OOyEhdOPaqAaAG1jRv/Zwbg== + dependencies: + acorn "^8.14.0" + acorn-jsx "^5.3.2" + acorn-jsx-walk "^2.0.0" + acorn-loose "^8.4.0" + acorn-walk "^8.3.4" + ajv "^8.17.1" + commander "^12.1.0" + enhanced-resolve "^5.17.1" + ignore "^6.0.2" + interpret "^3.1.1" + is-installed-globally "^1.0.0" + json5 "^2.2.3" + memoize "^10.0.0" + picocolors "^1.1.1" + picomatch "^4.0.2" + prompts "^2.4.2" + rechoir "^0.8.0" + safe-regex "^2.1.1" + semver "^7.6.3" + teamcity-service-messages "^0.1.14" + tsconfig-paths-webpack-plugin "^4.2.0" + watskeburt "^4.1.1" + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detect-libc@^2.0.0: + version "2.0.3" + resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz" + integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== + +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +docker-modem@^5.0.3: + version "5.0.3" + resolved "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.3.tgz" + integrity sha512-89zhop5YVhcPEt5FpUFGr3cDyceGhq/F9J+ZndQ4KfqNvfbJpPMfgeixFgUj5OjCYAboElqODxY5Z1EBsSa6sg== + dependencies: + debug "^4.1.1" + readable-stream "^3.5.0" + split-ca "^1.0.1" + ssh2 "^1.15.0" + +dockerode@^4.0.2: + version "4.0.2" + resolved "https://registry.npmjs.org/dockerode/-/dockerode-4.0.2.tgz" + integrity sha512-9wM1BVpVMFr2Pw3eJNXrYYt6DT9k0xMcsSCjtPvyQ+xa1iPg/Mo3T/gUcwI0B2cczqCeCYRPF8yFYDwtFXT0+w== + dependencies: + "@balena/dockerignore" "^1.0.2" + docker-modem "^5.0.3" + tar-fs "~2.0.1" + +doctrine@3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +dunder-proto@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.0.tgz" + integrity sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A== + dependencies: + call-bind-apply-helpers "^1.0.0" + es-errors "^1.3.0" + gopd "^1.2.0" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +emoji-regex@^10.3.0: + version "10.4.0" + resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz" + integrity sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +enabled@2.0.x: + version "2.0.0" + resolved "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz" + integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +encodeurl@~2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + +encoding@^0.1.0, encoding@^0.1.12: + version "0.1.13" + resolved "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz" + integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== + dependencies: + iconv-lite "^0.6.2" + +end-of-stream@^1.1.0, end-of-stream@^1.4.1: + version "1.4.4" + resolved "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +enhanced-resolve@^5.17.1, enhanced-resolve@^5.7.0: + version "5.17.1" + resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz" + integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +env-paths@^2.2.0: + version "2.2.1" + resolved "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz" + integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== + +err-code@^2.0.2: + version "2.0.3" + resolved "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz" + integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== + +es-define-property@^1.0.0, es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +esbuild@~0.23.0: + version "0.23.1" + resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz" + integrity sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg== + optionalDependencies: + "@esbuild/aix-ppc64" "0.23.1" + "@esbuild/android-arm" "0.23.1" + "@esbuild/android-arm64" "0.23.1" + "@esbuild/android-x64" "0.23.1" + "@esbuild/darwin-arm64" "0.23.1" + "@esbuild/darwin-x64" "0.23.1" + "@esbuild/freebsd-arm64" "0.23.1" + "@esbuild/freebsd-x64" "0.23.1" + "@esbuild/linux-arm" "0.23.1" + "@esbuild/linux-arm64" "0.23.1" + "@esbuild/linux-ia32" "0.23.1" + "@esbuild/linux-loong64" "0.23.1" + "@esbuild/linux-mips64el" "0.23.1" + "@esbuild/linux-ppc64" "0.23.1" + "@esbuild/linux-riscv64" "0.23.1" + "@esbuild/linux-s390x" "0.23.1" + "@esbuild/linux-x64" "0.23.1" + "@esbuild/netbsd-x64" "0.23.1" + "@esbuild/openbsd-arm64" "0.23.1" + "@esbuild/openbsd-x64" "0.23.1" + "@esbuild/sunos-x64" "0.23.1" + "@esbuild/win32-arm64" "0.23.1" + "@esbuild/win32-ia32" "0.23.1" + "@esbuild/win32-x64" "0.23.1" + +escalade@^3.1.1: + version "3.2.0" + resolved "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +expand-template@^2.0.3: + version "2.0.3" + resolved "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz" + integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== + +express-rate-limit@^7.4.1: + version "7.4.1" + resolved "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.4.1.tgz" + integrity sha512-KS3efpnpIDVIXopMc65EMbWbUht7qvTCdtCR2dD/IZmi9MIkopYESwyRqLgv8Pfu589+KqDqOdzJWW7AHoACeg== + +express@^4.21.1, "express@>=4.0.0 || >=5.0.0-beta", "express@4 || 5 || ^5.0.0-beta.1": + version "4.21.2" + resolved "https://registry.npmjs.org/express/-/express-4.21.2.tgz" + integrity sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.3" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.7.1" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~2.0.0" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.3.1" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.3" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.12" + proxy-addr "~2.0.7" + qs "6.13.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.19.0" + serve-static "1.16.2" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.2.9: + version "3.3.2" + resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fast-uri@^3.0.1: + version "3.0.3" + resolved "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz" + integrity sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw== + +fastq@^1.6.0: + version "1.17.1" + resolved "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz" + integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== + dependencies: + reusify "^1.0.4" + +fecha@^4.2.0: + version "4.2.3" + resolved "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz" + integrity sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw== + +fetch-blob@^3.1.2, fetch-blob@^3.1.4: + version "3.2.0" + resolved "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz" + integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ== + dependencies: + node-domexception "^1.0.0" + web-streams-polyfill "^3.0.3" + +file-uri-to-path@1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz" + integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.3.1: + version "1.3.1" + resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz" + integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ== + dependencies: + debug "2.6.9" + encodeurl "~2.0.0" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +fn.name@1.x.x: + version "1.1.0" + resolved "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz" + integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== + +form-data@^4.0.0: + version "4.0.1" + resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz" + integrity sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +formdata-polyfill@^4.0.10: + version "4.0.10" + resolved "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz" + integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g== + dependencies: + fetch-blob "^3.1.2" + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +from2@^2.3.0: + version "2.3.0" + resolved "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz" + integrity sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g== + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.0" + +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz" + integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== + +fs-extra@^9.1.0: + version "9.1.0" + resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz" + integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== + dependencies: + at-least-node "^1.0.0" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-minipass@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +gauge@^3.0.0: + version "3.0.2" + resolved "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz" + integrity sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q== + dependencies: + aproba "^1.0.3 || ^2.0.0" + color-support "^1.1.2" + console-control-strings "^1.0.0" + has-unicode "^2.0.1" + object-assign "^4.1.1" + signal-exit "^3.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" + wide-align "^1.1.2" + +gauge@^4.0.3: + version "4.0.4" + resolved "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz" + integrity sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg== + dependencies: + aproba "^1.0.3 || ^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" + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-east-asian-width@^1.0.0: + version "1.3.0" + resolved "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz" + integrity sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ== + +get-intrinsic@^1.2.4: + version "1.2.5" + resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.5.tgz" + integrity sha512-Y4+pKa7XeRUPWFNvOOYHkRYrfzW07oraURSvjDmRVOJ748OrVmeXtpE4+GCEHncjCjkTxPNRt8kEbxDhsn6VTg== + dependencies: + call-bind-apply-helpers "^1.0.0" + dunder-proto "^1.0.0" + es-define-property "^1.0.1" + es-errors "^1.3.0" + function-bind "^1.1.2" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + +get-tsconfig@^4.7.5: + version "4.8.1" + resolved "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz" + integrity sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg== + dependencies: + resolve-pkg-maps "^1.0.0" + +github-from-package@0.0.0: + version "0.0.0" + resolved "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz" + integrity sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw== + +glob-parent@^5.1.2, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob@^7.1.3, glob@^7.1.4: + version "7.2.3" + resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@7.1.6: + version "7.1.6" + resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +global-directory@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz" + integrity sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q== + dependencies: + ini "4.1.1" + +globby@^11.1.0: + version "11.1.0" + resolved "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" + +gopd@^1.0.1, gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + +graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6: + version "4.2.11" + resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + +has-unicode@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz" + integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== + +has@^1.0.3: + version "1.0.4" + resolved "https://registry.npmjs.org/has/-/has-1.0.4.tgz" + integrity sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ== + +hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +http-cache-semantics@^4.1.0: + version "4.1.1" + resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz" + integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-proxy-agent@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz" + integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== + dependencies: + "@tootallnate/once" "1" + agent-base "6" + debug "4" + +https-proxy-agent@^5.0.0: + version "5.0.1" + resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== + dependencies: + agent-base "6" + debug "4" + +https@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/https/-/https-1.0.0.tgz" + integrity sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg== + +humanize-ms@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz" + integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ== + dependencies: + ms "^2.0.0" + +iconv-lite@^0.6.2: + version "0.6.3" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ieee754@^1.1.13: + version "1.2.1" + resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +ignore-by-default@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz" + integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== + +ignore@^5.2.0: + version "5.3.2" + resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== + +ignore@^6.0.2: + version "6.0.2" + resolved "https://registry.npmjs.org/ignore/-/ignore-6.0.2.tgz" + integrity sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A== + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + +infer-owner@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz" + integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3, inherits@2, inherits@2.0.4: + version "2.0.4" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ini@~1.3.0: + version "1.3.8" + resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + +ini@4.1.1: + version "4.1.1" + resolved "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz" + integrity sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g== + +interpret@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz" + integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ== + +into-stream@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/into-stream/-/into-stream-6.0.0.tgz" + integrity sha512-XHbaOAvP+uFKUFsOgoNPRjLkwB+I22JFPFe5OjTkQ0nwgj6+pSjb4NmB6VMxaPshLiOf+zcpOCBQuLwC1KHhZA== + dependencies: + from2 "^2.3.0" + p-is-promise "^3.0.0" + +ip-address@^9.0.5: + version "9.0.5" + resolved "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz" + integrity sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g== + dependencies: + jsbn "1.1.0" + sprintf-js "^1.1.3" + +ipaddr.js@^2.2.0: + version "2.2.0" + resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz" + integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-arrayish@^0.3.1: + version "0.3.2" + resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz" + integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-core-module@^2.13.0: + version "2.15.1" + resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz" + integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== + dependencies: + hasown "^2.0.2" + +is-core-module@2.9.0: + version "2.9.0" + resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz" + integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A== + dependencies: + has "^1.0.3" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-installed-globally@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-1.0.0.tgz" + integrity sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ== + dependencies: + global-directory "^4.0.1" + is-path-inside "^4.0.0" + +is-interactive@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz" + integrity sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ== + +is-lambda@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz" + integrity sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ== + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-path-inside@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz" + integrity sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA== + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +is-unicode-supported@^1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz" + integrity sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ== + +is-unicode-supported@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz" + integrity sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ== + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +jsbn@1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz" + integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A== + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +json5@^2.2.2, json5@^2.2.3: + version "2.2.3" + resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +kleur@^3.0.3: + version "3.0.3" + resolved "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz" + integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== + +kuler@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz" + integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A== + +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz" + integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ== + +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz" + integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== + +lodash.mergewith@^4.6.2: + version "4.6.2" + resolved "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz" + integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== + +log-symbols@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz" + integrity sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw== + dependencies: + chalk "^5.3.0" + is-unicode-supported "^1.3.0" + +logform@^2.7.0: + version "2.7.0" + resolved "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz" + integrity sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ== + dependencies: + "@colors/colors" "1.6.0" + "@types/triple-beam" "^1.3.2" + fecha "^4.2.0" + ms "^2.1.1" + safe-stable-stringify "^2.3.1" + triple-beam "^1.3.0" + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +make-dir@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== + dependencies: + semver "^6.0.0" + +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +make-fetch-happen@^9.1.0: + version "9.1.0" + resolved "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz" + integrity sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg== + dependencies: + agentkeepalive "^4.1.3" + cacache "^15.2.0" + http-cache-semantics "^4.1.0" + http-proxy-agent "^4.0.1" + https-proxy-agent "^5.0.0" + is-lambda "^1.0.1" + lru-cache "^6.0.0" + minipass "^3.1.3" + minipass-collect "^1.0.2" + minipass-fetch "^1.3.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + negotiator "^0.6.2" + promise-retry "^2.0.1" + socks-proxy-agent "^6.0.0" + ssri "^8.0.0" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +memoize@^10.0.0: + version "10.0.0" + resolved "https://registry.npmjs.org/memoize/-/memoize-10.0.0.tgz" + integrity sha512-H6cBLgsi6vMWOcCpvVCdFFnl3kerEXbrYh9q+lY6VXvQSmM6CkmV08VOwT+WE2tzIEqRPFfAq3fm4v/UIW6mSA== + dependencies: + mimic-function "^5.0.0" + +merge-descriptors@1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz" + integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== + +merge2@^1.3.0, merge2@^1.4.1: + version "1.4.1" + resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +micromatch@^4.0.4: + version "4.0.8" + resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mimic-function@^5.0.0: + version "5.0.1" + resolved "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz" + integrity sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA== + +mimic-response@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz" + integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== + +minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +minipass-collect@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz" + integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA== + dependencies: + minipass "^3.0.0" + +minipass-fetch@^1.3.2: + version "1.4.1" + resolved "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz" + integrity sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw== + dependencies: + minipass "^3.1.0" + minipass-sized "^1.0.3" + minizlib "^2.0.0" + optionalDependencies: + encoding "^0.1.12" + +minipass-flush@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz" + integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw== + dependencies: + minipass "^3.0.0" + +minipass-pipeline@^1.2.2, minipass-pipeline@^1.2.4: + version "1.2.4" + resolved "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz" + integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== + dependencies: + minipass "^3.0.0" + +minipass-sized@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz" + integrity sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g== + dependencies: + minipass "^3.0.0" + +minipass@^3.0.0, minipass@^3.1.0, minipass@^3.1.1, minipass@^3.1.3: + version "3.3.6" + resolved "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz" + integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== + dependencies: + yallist "^4.0.0" + +minipass@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz" + integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== + +minizlib@^2.0.0, minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + +mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: + version "0.5.3" + resolved "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz" + integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== + +mkdirp@^1.0.3, mkdirp@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + +ms@^2.0.0, ms@^2.1.1, ms@^2.1.3, ms@2.1.3: + version "2.1.3" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +multistream@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/multistream/-/multistream-4.1.0.tgz" + integrity sha512-J1XDiAmmNpRCBfIWJv+n0ymC4ABcf/Pl+5YvC5B/D2f/2+8PtHvCNxMPKiQcZyi922Hq69J2YOpb1pTywfifyw== + dependencies: + once "^1.4.0" + readable-stream "^3.6.0" + +nan@^2.19.0, nan@^2.20.0: + version "2.22.0" + resolved "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz" + integrity sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw== + +napi-build-utils@^1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz" + integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== + +negotiator@^0.6.2, negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +node-abi@^3.3.0: + version "3.71.0" + resolved "https://registry.npmjs.org/node-abi/-/node-abi-3.71.0.tgz" + integrity sha512-SZ40vRiy/+wRTf21hxkkEjPJZpARzUMVcJoQse2EF8qkUWbbO2z7vd5oA/H6bVH6SZQ5STGcu0KRDS7biNRfxw== + dependencies: + semver "^7.3.5" + +node-addon-api@^5.0.0: + version "5.1.0" + resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz" + integrity sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA== + +node-addon-api@^7.0.0: + version "7.1.1" + resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz" + integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ== + +node-domexception@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz" + integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== + +node-fetch@^2.6.6: + version "2.7.0" + resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + +node-fetch@^2.6.7: + version "2.7.0" + resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + +node-fetch@^3.3.2: + version "3.3.2" + resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz" + integrity sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA== + dependencies: + data-uri-to-buffer "^4.0.0" + fetch-blob "^3.1.4" + formdata-polyfill "^4.0.10" + +node-gyp@8.x: + version "8.4.1" + resolved "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz" + integrity sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w== + dependencies: + env-paths "^2.2.0" + glob "^7.1.4" + graceful-fs "^4.2.6" + make-fetch-happen "^9.1.0" + nopt "^5.0.0" + npmlog "^6.0.0" + rimraf "^3.0.2" + semver "^7.3.5" + tar "^6.1.2" + which "^2.0.2" + +nodemailer@^6.9.16: + version "6.9.16" + resolved "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.16.tgz" + integrity sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ== + +nodemon@^3.1.7: + version "3.1.7" + resolved "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz" + integrity sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ== + dependencies: + chokidar "^3.5.2" + debug "^4" + ignore-by-default "^1.0.1" + minimatch "^3.1.2" + pstree.remy "^1.1.8" + semver "^7.5.3" + simple-update-notifier "^2.0.0" + supports-color "^5.5.0" + touch "^3.1.0" + undefsafe "^2.0.5" + +nopt@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz" + integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== + dependencies: + abbrev "1" + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +npmlog@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz" + integrity sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw== + dependencies: + are-we-there-yet "^2.0.0" + console-control-strings "^1.1.0" + gauge "^3.0.0" + set-blocking "^2.0.0" + +npmlog@^6.0.0: + version "6.0.2" + resolved "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz" + integrity sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg== + dependencies: + are-we-there-yet "^3.0.0" + console-control-strings "^1.1.0" + gauge "^4.0.3" + set-blocking "^2.0.0" + +object-assign@^4, object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-inspect@^1.13.1: + version "1.13.3" + resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz" + integrity sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA== + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +one-time@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz" + integrity sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g== + dependencies: + fn.name "1.x.x" + +onetime@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz" + integrity sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ== + dependencies: + mimic-function "^5.0.0" + +openapi-types@>=7: + version "12.1.3" + resolved "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz" + integrity sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw== + +ora@^8.1.1: + version "8.1.1" + resolved "https://registry.npmjs.org/ora/-/ora-8.1.1.tgz" + integrity sha512-YWielGi1XzG1UTvOaCFaNgEnuhZVMSHYkW/FQ7UX8O26PtlpdM84c0f7wLPlkvx2RfiQmnzd61d/MGxmpQeJPw== + dependencies: + chalk "^5.3.0" + cli-cursor "^5.0.0" + cli-spinners "^2.9.2" + is-interactive "^2.0.0" + is-unicode-supported "^2.0.0" + log-symbols "^6.0.0" + stdin-discarder "^0.2.2" + string-width "^7.2.0" + strip-ansi "^7.1.0" + +p-is-promise@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/p-is-promise/-/p-is-promise-3.0.0.tgz" + integrity sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ== + +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-to-regexp@0.1.12: + version "0.1.12" + resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz" + integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ== + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +picomatch@^2.0.4: + version "2.3.1" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +picomatch@^2.2.1: + version "2.3.1" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +picomatch@^4.0.2: + version "4.0.2" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz" + integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg== + +pkg-fetch@3.4.2: + version "3.4.2" + resolved "https://registry.npmjs.org/pkg-fetch/-/pkg-fetch-3.4.2.tgz" + integrity sha512-0+uijmzYcnhC0hStDjm/cl2VYdrmVVBpe7Q8k9YBojxmR5tG8mvR9/nooQq3QSXiQqORDVOTY3XqMEqJVIzkHA== + dependencies: + chalk "^4.1.2" + fs-extra "^9.1.0" + https-proxy-agent "^5.0.0" + node-fetch "^2.6.6" + progress "^2.0.3" + semver "^7.3.5" + tar-fs "^2.1.1" + yargs "^16.2.0" + +pkg@^5.8.1: + version "5.8.1" + resolved "https://registry.npmjs.org/pkg/-/pkg-5.8.1.tgz" + integrity sha512-CjBWtFStCfIiT4Bde9QpJy0KeH19jCfwZRJqHFDFXfhUklCx8JoFmMj3wgnEYIwGmZVNkhsStPHEOnrtrQhEXA== + dependencies: + "@babel/generator" "7.18.2" + "@babel/parser" "7.18.4" + "@babel/types" "7.19.0" + chalk "^4.1.2" + fs-extra "^9.1.0" + globby "^11.1.0" + into-stream "^6.0.0" + is-core-module "2.9.0" + minimist "^1.2.6" + multistream "^4.1.0" + pkg-fetch "3.4.2" + prebuild-install "7.1.1" + resolve "^1.22.0" + stream-meter "^1.0.4" + +playwright-core@1.49.0: + version "1.49.0" + resolved "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0.tgz" + integrity sha512-R+3KKTQF3npy5GTiKH/T+kdhoJfJojjHESR1YEWhYuEKRVfVaxH3+4+GvXE5xyCngCxhxnykk0Vlah9v8fs3jA== + +playwright@1.49.0: + version "1.49.0" + resolved "https://registry.npmjs.org/playwright/-/playwright-1.49.0.tgz" + integrity sha512-eKpmys0UFDnfNb3vfsf8Vx2LEOtflgRebl0Im2eQQnYMA4Aqd+Zw8bEOB+7ZKvN76901mRnqdsiOGKxzVTbi7A== + dependencies: + playwright-core "1.49.0" + optionalDependencies: + fsevents "2.3.2" + +prebuild-install@^7.1.1: + version "7.1.2" + resolved "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz" + integrity sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ== + dependencies: + detect-libc "^2.0.0" + expand-template "^2.0.3" + github-from-package "0.0.0" + minimist "^1.2.3" + mkdirp-classic "^0.5.3" + napi-build-utils "^1.0.1" + node-abi "^3.3.0" + pump "^3.0.0" + rc "^1.2.7" + simple-get "^4.0.0" + tar-fs "^2.0.0" + tunnel-agent "^0.6.0" + +prebuild-install@7.1.1: + version "7.1.1" + resolved "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz" + integrity sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw== + dependencies: + detect-libc "^2.0.0" + expand-template "^2.0.3" + github-from-package "0.0.0" + minimist "^1.2.3" + mkdirp-classic "^0.5.3" + napi-build-utils "^1.0.1" + node-abi "^3.3.0" + pump "^3.0.0" + rc "^1.2.7" + simple-get "^4.0.0" + tar-fs "^2.0.0" + tunnel-agent "^0.6.0" + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +progress@^2.0.3: + version "2.0.3" + resolved "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + +promise-inflight@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz" + integrity sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g== + +promise-retry@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz" + integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g== + dependencies: + err-code "^2.0.2" + retry "^0.12.0" + +prompts@^2.4.2: + version "2.4.2" + resolved "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz" + integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== + dependencies: + kleur "^3.0.3" + sisteransi "^1.0.5" + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +pstree.remy@^1.1.8: + version "1.1.8" + resolved "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz" + integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== + +pump@^3.0.0: + version "3.0.2" + resolved "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz" + integrity sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +qs@6.13.0: + version "6.13.0" + resolved "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz" + integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== + dependencies: + side-channel "^1.0.6" + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +rc@^1.2.7: + version "1.2.8" + resolved "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +readable-stream@^2.0.0: + version "2.3.8" + resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^2.1.4: + version "2.3.8" + resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0, readable-stream@^3.6.2: + version "3.6.2" + resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdirp@^4.0.1: + version "4.0.2" + resolved "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz" + integrity sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA== + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +rechoir@^0.8.0: + version "0.8.0" + resolved "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz" + integrity sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ== + dependencies: + resolve "^1.20.0" + +regexp-tree@~0.1.1: + version "0.1.27" + resolved "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz" + integrity sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA== + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +resolve-pkg-maps@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz" + integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== + +resolve@^1.20.0, resolve@^1.22.0: + version "1.22.8" + resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +restore-cursor@^5.0.0: + version "5.1.0" + resolved "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz" + integrity sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA== + dependencies: + onetime "^7.0.0" + signal-exit "^4.1.0" + +retry@^0.12.0: + version "0.12.0" + resolved "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz" + integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +safe-buffer@^5.0.1, safe-buffer@~5.2.0, safe-buffer@5.2.1: + version "5.2.1" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-regex@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz" + integrity sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A== + dependencies: + regexp-tree "~0.1.1" + +safe-stable-stringify@^2.3.1: + version "2.5.0" + resolved "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz" + integrity sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA== + +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +semver@^6.0.0: + version "6.3.1" + resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^7.3.5, semver@^7.5.3, semver@^7.6.3: + version "7.6.3" + resolved "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + +send@0.19.0: + version "0.19.0" + resolved "https://registry.npmjs.org/send/-/send-0.19.0.tgz" + integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== + 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" + +serve-static@1.16.2: + version "1.16.2" + resolved "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz" + integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== + dependencies: + encodeurl "~2.0.0" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.19.0" + +set-blocking@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz" + integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== + +set-function-length@^1.2.2: + version "1.2.2" + resolved "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +side-channel@^1.0.6: + version "1.0.6" + resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + +signal-exit@^3.0.0, signal-exit@^3.0.7: + version "3.0.7" + resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +signal-exit@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + +simple-concat@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz" + integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== + +simple-get@^4.0.0: + version "4.0.1" + resolved "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz" + integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA== + dependencies: + decompress-response "^6.0.0" + once "^1.3.1" + simple-concat "^1.0.0" + +simple-swizzle@^0.2.2: + version "0.2.2" + resolved "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz" + integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg== + dependencies: + is-arrayish "^0.3.1" + +simple-update-notifier@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz" + integrity sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w== + dependencies: + semver "^7.5.3" + +sisteransi@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz" + integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +smart-buffer@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz" + integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== + +socks-proxy-agent@^6.0.0: + version "6.2.1" + resolved "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz" + integrity sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ== + dependencies: + agent-base "^6.0.2" + debug "^4.3.3" + socks "^2.6.2" + +socks@^2.6.2: + version "2.8.3" + resolved "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz" + integrity sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw== + dependencies: + ip-address "^9.0.5" + smart-buffer "^4.2.0" + +split-ca@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz" + integrity sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ== + +sprintf-js@^1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz" + integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== + +sqlite3@^5.1.7: + version "5.1.7" + resolved "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz" + integrity sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog== + dependencies: + bindings "^1.5.0" + node-addon-api "^7.0.0" + prebuild-install "^7.1.1" + tar "^6.1.11" + optionalDependencies: + node-gyp "8.x" + +ssh2@^1.15.0: + version "1.16.0" + resolved "https://registry.npmjs.org/ssh2/-/ssh2-1.16.0.tgz" + integrity sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg== + dependencies: + asn1 "^0.2.6" + bcrypt-pbkdf "^1.0.2" + optionalDependencies: + cpu-features "~0.0.10" + nan "^2.20.0" + +ssri@^8.0.0, ssri@^8.0.1: + version "8.0.1" + resolved "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz" + integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ== + dependencies: + minipass "^3.1.1" + +stack-trace@0.0.x: + version "0.0.10" + resolved "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz" + integrity sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg== + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +stdin-discarder@^0.2.2: + version "0.2.2" + resolved "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz" + integrity sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ== + +stream-meter@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/stream-meter/-/stream-meter-1.0.4.tgz" + integrity sha512-4sOEtrbgFotXwnEuzzsQBYEV1elAeFSO8rSGeTwabuX1RRn/kEq9JVH7I0MRBhKVRR0sJkr0M0QCH7yOLf9fhQ== + dependencies: + readable-stream "^2.1.4" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^7.2.0: + version "7.2.0" + resolved "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz" + integrity sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ== + dependencies: + emoji-regex "^10.3.0" + get-east-asian-width "^1.0.0" + strip-ansi "^7.1.0" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^7.1.0: + version "7.1.0" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + dependencies: + ansi-regex "^6.0.1" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz" + integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz" + integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== + +supports-color@^5.5.0: + version "5.5.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +swagger-jsdoc@^6.2.8: + version "6.2.8" + resolved "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz" + integrity sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ== + dependencies: + commander "6.2.0" + doctrine "3.0.0" + glob "7.1.6" + lodash.mergewith "^4.6.2" + swagger-parser "^10.0.3" + yaml "2.0.0-1" + +swagger-parser@^10.0.3: + version "10.0.3" + resolved "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz" + integrity sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg== + dependencies: + "@apidevtools/swagger-parser" "10.0.3" + +swagger-ui-dist@>=5.0.0: + version "5.18.2" + resolved "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.18.2.tgz" + integrity sha512-J+y4mCw/zXh1FOj5wGJvnAajq6XgHOyywsa9yITmwxIlJbMqITq3gYRZHaeqLVH/eV/HOPphE6NjF+nbSNC5Zw== + dependencies: + "@scarf/scarf" "=1.4.0" + +swagger-ui-express@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz" + integrity sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA== + dependencies: + swagger-ui-dist ">=5.0.0" + +tapable@^2.2.0, tapable@^2.2.1: + version "2.2.1" + resolved "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + +tar-fs@^2.0.0, tar-fs@~2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz" + integrity sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA== + dependencies: + chownr "^1.1.1" + mkdirp-classic "^0.5.2" + pump "^3.0.0" + tar-stream "^2.0.0" + +tar-fs@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz" + integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== + dependencies: + chownr "^1.1.1" + mkdirp-classic "^0.5.2" + pump "^3.0.0" + tar-stream "^2.1.4" + +tar-stream@^2.0.0, tar-stream@^2.1.4: + version "2.2.0" + resolved "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz" + integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== + dependencies: + bl "^4.0.3" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" + +tar@^6.0.2, tar@^6.1.11, tar@^6.1.2: + version "6.2.1" + resolved "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz" + integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^5.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + +teamcity-service-messages@^0.1.14: + version "0.1.14" + resolved "https://registry.npmjs.org/teamcity-service-messages/-/teamcity-service-messages-0.1.14.tgz" + integrity sha512-29aQwaHqm8RMX74u2o/h1KbMLP89FjNiMxD9wbF2BbWOnbM+q+d1sCEC+MqCc4QW3NJykn77OMpTFw/xTHIc0w== + +text-hex@1.0.x: + version "1.0.0" + resolved "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz" + integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg== + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz" + integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +touch@^3.1.0: + version "3.1.1" + resolved "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz" + integrity sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA== + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + +triple-beam@^1.3.0: + version "1.4.1" + resolved "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz" + integrity sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg== + +ts-node@^10.9.2: + version "10.9.2" + resolved "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz" + integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + +tsconfig-paths-webpack-plugin@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz" + integrity sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA== + dependencies: + chalk "^4.1.0" + enhanced-resolve "^5.7.0" + tapable "^2.2.1" + tsconfig-paths "^4.1.2" + +tsconfig-paths@^4.1.2: + version "4.2.0" + resolved "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz" + integrity sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg== + dependencies: + json5 "^2.2.2" + minimist "^1.2.6" + strip-bom "^3.0.0" + +tsx@^4.19.2: + version "4.19.2" + resolved "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz" + integrity sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g== + dependencies: + esbuild "~0.23.0" + get-tsconfig "^4.7.5" + optionalDependencies: + fsevents "~2.3.3" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz" + integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w== + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3: + version "0.14.5" + resolved "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz" + integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typescript@>=2.7: + version "5.7.2" + resolved "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz" + integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg== + +uglify-js@^3.19.3: + version "3.19.3" + resolved "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz" + integrity sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ== + +undefsafe@^2.0.5: + version "2.0.5" + resolved "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz" + integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + +undici-types@~6.20.0: + version "6.20.0" + resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz" + integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== + +unique-filename@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz" + integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ== + dependencies: + unique-slug "^2.0.0" + +unique-slug@^2.0.0: + version "2.0.2" + resolved "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz" + integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w== + dependencies: + imurmurhash "^0.1.4" + +universalify@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== + +unpipe@~1.0.0, unpipe@1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + +validator@^13.7.0: + version "13.12.0" + resolved "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz" + integrity sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg== + +vary@^1, vary@~1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +watskeburt@^4.1.1: + version "4.2.2" + resolved "https://registry.npmjs.org/watskeburt/-/watskeburt-4.2.2.tgz" + integrity sha512-AOCg1UYxWpiHW1tUwqpJau8vzarZYTtzl2uu99UptBmbzx6kOzCGMfRLF6KIRX4PYekmryn89MzxlRNkL66YyA== + +web-streams-polyfill@^3.0.3: + version "3.3.3" + resolved "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz" + integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw== + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +which@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wide-align@^1.1.2, wide-align@^1.1.5: + version "1.1.5" + resolved "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz" + integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== + dependencies: + string-width "^1.0.2 || 2 || 3 || 4" + +winston-transport@^4.9.0: + version "4.9.0" + resolved "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz" + integrity sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A== + dependencies: + logform "^2.7.0" + readable-stream "^3.6.2" + triple-beam "^1.3.0" + +winston@^3.15.0: + version "3.17.0" + resolved "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz" + integrity sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw== + dependencies: + "@colors/colors" "^1.6.0" + "@dabh/diagnostics" "^2.0.2" + async "^3.2.3" + is-stream "^2.0.0" + logform "^2.7.0" + one-time "^1.0.0" + readable-stream "^3.4.0" + safe-stable-stringify "^2.3.1" + stack-trace "0.0.x" + triple-beam "^1.3.0" + winston-transport "^4.9.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yaml@2.0.0-1: + version "2.0.0-1" + resolved "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz" + integrity sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ== + +yargs-parser@^20.2.2: + version "20.2.9" + resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + +yargs@^16.2.0: + version "16.2.0" + resolved "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + +z-schema@^5.0.1: + version "5.0.5" + resolved "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz" + integrity sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q== + dependencies: + lodash.get "^4.4.2" + lodash.isequal "^4.5.0" + validator "^13.7.0" + optionalDependencies: + commander "^9.4.1" From 84c3e993a69d6accfc3b7d31966874cf6b792829 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 20 Dec 2024 22:06:47 +0100 Subject: [PATCH 018/324] Fix: Disabled image pushing for PRs running against dev branch --- .github/workflows/test-build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-build.yaml b/.github/workflows/test-build.yaml index fb47183..8c805d4 100644 --- a/.github/workflows/test-build.yaml +++ b/.github/workflows/test-build.yaml @@ -52,7 +52,7 @@ jobs: uses: docker/build-push-action@v5 with: platforms: linux/amd64,linux/arm64 - push: true + push: false tags: ${{ steps.metadata.outputs.tags }} labels: ${{ steps.metadata.outputs.labels }} cache-from: type=gha From 49b69867db068ee38a63f6152bae6b81e05b45bb Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sat, 21 Dec 2024 10:51:48 +0100 Subject: [PATCH 019/324] Fix: Dropping linux/arm/v7 due to workflow timeout (sorry guys) --- .github/workflows/build-dev.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-dev.yaml b/.github/workflows/build-dev.yaml index afc8ffc..09f9b0e 100644 --- a/.github/workflows/build-dev.yaml +++ b/.github/workflows/build-dev.yaml @@ -39,7 +39,7 @@ jobs: - name: Build and push uses: docker/build-push-action@v5 with: - platforms: linux/amd64,linux/arm64,linux/arm/v7 + platforms: linux/amd64,linux/arm64, push: true tags: ${{ steps.metadata.outputs.tags }} labels: ${{ steps.metadata.outputs.labels }} From a2618b8ea170c4ad2ec2c9a7af9ab327d3a88056 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sat, 21 Dec 2024 12:03:24 +0100 Subject: [PATCH 020/324] Chore: Add exiting log --- package-lock.json | 898 ---------------------- package.json | 1 - src/init.ts | 29 + src/misc/dependencyGraphs/mermaid-all.txt | 197 ++--- src/utils/removeUnusedDeps.sh | 2 +- yarn.lock | 470 +---------- 6 files changed, 150 insertions(+), 1447 deletions(-) diff --git a/package-lock.json b/package-lock.json index 641c0d3..dcd2ac0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,7 +40,6 @@ "dependency-cruiser": "^16.5.0", "nodemon": "^3.1.7", "ora": "^8.1.1", - "pkg": "^5.8.1", "ts-node": "^10.9.2", "tsx": "^4.19.2", "uglify-js": "^3.19.3" @@ -93,69 +92,6 @@ "openapi-types": ">=7" } }, - "node_modules/@babel/generator": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.2.tgz", - "integrity": "sha512-W1lG5vUwFvfMd8HVXqdfbuG7RuaSrTCCD8cl8fP8wOivdbtbIg2Db3IWUcgvfxKbbn6ZBGYRW/Zk1MIwK49mgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.18.2", - "@jridgewell/gen-mapping": "^0.3.0", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.18.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.4.tgz", - "integrity": "sha512-FDge0dFazETFcxGw/EXzOkN8uJp0PC7Qbm+Pe9T+av2zlBpOgunFHkQPPn+eRuClU73JF+98D531UgayY89tow==", - "dev": true, - "license": "MIT", - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/types": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.19.0.tgz", - "integrity": "sha512-YuGopBq3ke25BVSiS6fgF49Ul9gH1x70Bcr6bqRLjWCkcX8Hre1/5+z+IiWOIerRMSSEfGZVB9z9kyq7wVs9YA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.18.10", - "@babel/helper-validator-identifier": "^7.18.6", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@balena/dockerignore": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz", @@ -610,32 +546,6 @@ "license": "MIT", "optional": true }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -646,16 +556,6 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", @@ -720,44 +620,6 @@ } } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/@npmcli/fs": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", @@ -1311,16 +1173,6 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -1343,16 +1195,6 @@ "dev": true, "license": "MIT" }, - "node_modules/at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1682,18 +1524,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, "node_modules/color": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", @@ -1819,13 +1649,6 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, - "license": "MIT" - }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -2023,19 +1846,6 @@ "node": ">=0.3.1" } }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/docker-modem": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.3.tgz", @@ -2239,16 +2049,6 @@ "@esbuild/win32-x64": "0.23.1" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -2365,23 +2165,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, "node_modules/fast-uri": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", @@ -2389,16 +2172,6 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, "node_modules/fecha": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", @@ -2531,72 +2304,12 @@ "node": ">= 0.6" } }, - "node_modules/from2": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", - "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.0" - } - }, - "node_modules/from2/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/from2/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/from2/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "license": "MIT" }, - "node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -2660,16 +2373,6 @@ "node": ">=10" } }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, "node_modules/get-east-asian-width": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", @@ -2774,37 +2477,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globby/node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -2824,16 +2496,6 @@ "devOptional": true, "license": "ISC" }, - "node_modules/has": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", - "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -3066,23 +2728,6 @@ "node": ">=10.13.0" } }, - "node_modules/into-stream": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-6.0.0.tgz", - "integrity": "sha512-XHbaOAvP+uFKUFsOgoNPRjLkwB+I22JFPFe5OjTkQ0nwgj6+pSjb4NmB6VMxaPshLiOf+zcpOCBQuLwC1KHhZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "from2": "^2.3.0", - "p-is-promise": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/ip-address": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", @@ -3258,13 +2903,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT" - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3291,19 +2929,6 @@ "license": "MIT", "optional": true }, - "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -3324,19 +2949,6 @@ "node": ">=6" } }, - "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -3524,16 +3136,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -3543,33 +3145,6 @@ "node": ">= 0.6" } }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -3768,31 +3343,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/multistream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/multistream/-/multistream-4.1.0.tgz", - "integrity": "sha512-J1XDiAmmNpRCBfIWJv+n0ymC4ABcf/Pl+5YvC5B/D2f/2+8PtHvCNxMPKiQcZyi922Hq69J2YOpb1pTywfifyw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "once": "^1.4.0", - "readable-stream": "^3.6.0" - } - }, "node_modules/nan": { "version": "2.22.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", @@ -4227,16 +3777,6 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/p-is-promise": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-3.0.0.tgz", - "integrity": "sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/p-map": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", @@ -4284,16 +3824,6 @@ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -4314,221 +3844,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pkg": { - "version": "5.8.1", - "resolved": "https://registry.npmjs.org/pkg/-/pkg-5.8.1.tgz", - "integrity": "sha512-CjBWtFStCfIiT4Bde9QpJy0KeH19jCfwZRJqHFDFXfhUklCx8JoFmMj3wgnEYIwGmZVNkhsStPHEOnrtrQhEXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/generator": "7.18.2", - "@babel/parser": "7.18.4", - "@babel/types": "7.19.0", - "chalk": "^4.1.2", - "fs-extra": "^9.1.0", - "globby": "^11.1.0", - "into-stream": "^6.0.0", - "is-core-module": "2.9.0", - "minimist": "^1.2.6", - "multistream": "^4.1.0", - "pkg-fetch": "3.4.2", - "prebuild-install": "7.1.1", - "resolve": "^1.22.0", - "stream-meter": "^1.0.4" - }, - "bin": { - "pkg": "lib-es5/bin.js" - }, - "peerDependencies": { - "node-notifier": ">=9.0.1" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/pkg-fetch": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/pkg-fetch/-/pkg-fetch-3.4.2.tgz", - "integrity": "sha512-0+uijmzYcnhC0hStDjm/cl2VYdrmVVBpe7Q8k9YBojxmR5tG8mvR9/nooQq3QSXiQqORDVOTY3XqMEqJVIzkHA==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.2", - "fs-extra": "^9.1.0", - "https-proxy-agent": "^5.0.0", - "node-fetch": "^2.6.6", - "progress": "^2.0.3", - "semver": "^7.3.5", - "tar-fs": "^2.1.1", - "yargs": "^16.2.0" - }, - "bin": { - "pkg-fetch": "lib-es5/bin.js" - } - }, - "node_modules/pkg-fetch/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/pkg-fetch/node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true, - "license": "ISC" - }, - "node_modules/pkg-fetch/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-fetch/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/pkg-fetch/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-fetch/node_modules/tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", - "dev": true, - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/pkg/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/pkg/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg/node_modules/is-core-module": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", - "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/pkg/node_modules/prebuild-install": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", - "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^1.0.1", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/pkg/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/playwright": { "version": "1.49.0", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0.tgz", @@ -4587,23 +3902,6 @@ "node": ">=10" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, - "license": "MIT" - }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -4693,27 +3991,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -4809,16 +4086,6 @@ "regexp-tree": "bin/regexp-tree" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -4897,17 +4164,6 @@ "node": ">= 4" } }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -4924,30 +4180,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -5195,16 +4427,6 @@ "dev": true, "license": "MIT" }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -5350,49 +4572,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/stream-meter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/stream-meter/-/stream-meter-1.0.4.tgz", - "integrity": "sha512-4sOEtrbgFotXwnEuzzsQBYEV1elAeFSO8rSGeTwabuX1RRn/kEq9JVH7I0MRBhKVRR0sJkr0M0QCH7yOLf9fhQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "readable-stream": "^2.1.4" - } - }, - "node_modules/stream-meter/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/stream-meter/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/stream-meter/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -5642,16 +4821,6 @@ "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", "license": "MIT" }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5941,16 +5110,6 @@ "imurmurhash": "^0.1.4" } }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -6099,40 +5258,12 @@ "node": ">= 12.0.0" } }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -6148,35 +5279,6 @@ "node": ">= 6" } }, - "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/package.json b/package.json index 78ac946..5e85ece 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,6 @@ "dependency-cruiser": "^16.5.0", "nodemon": "^3.1.7", "ora": "^8.1.1", - "pkg": "^5.8.1", "ts-node": "^10.9.2", "tsx": "^4.19.2", "uglify-js": "^3.19.3" diff --git a/src/init.ts b/src/init.ts index 3979eb6..6d3854a 100644 --- a/src/init.ts +++ b/src/init.ts @@ -1,4 +1,5 @@ import express, { Request, Response, NextFunction } from "express"; +import process from "node:process"; import swaggerDocs from "./utils/swaggerDocs"; import auth from "./routes/auth/routes"; import data from "./routes/data/routes"; @@ -13,6 +14,7 @@ import { limiter } from "./middleware/rateLimiter"; import { scheduleFetch } from "./controllers/scheduler"; import cors from "cors"; import { blockWhileLocked } from "./middleware/checkLock"; +import logger from "./utils/logger"; const initializeApp = (app: express.Application): void => { app.use(cors()); @@ -42,6 +44,33 @@ const initializeApp = (app: express.Application): void => { app.get("/", (req: Request, res: Response) => { res.redirect("/api-docs"); }); + + process.on("exit", (code: number) => { + logger.warn(`Server exiting (Code: ${code})`); + console.log(` + \u001b[1;31mThank you for using\u001b[0m + + \u001b[1;34m###### ###### #### ### ### #### ######### ###### #########\u001b[0m + \u001b[1;34m### ### ### ### ### ### ### ### ### ### ### ###\u001b[0m + \u001b[1;34m### ### ### ### ### ###### #### ### ### ### ###\u001b[0m + \u001b[1;34m### ### ### ### ### ### ### #### ### ############ ###\u001b[0m + \u001b[1;34m### ### ### ### ### ### ### #### ### ### ### ###\u001b[0m + \u001b[1;34m###### ###### #### ### ### #### ### ### ### ### \u001b[0m(\u001b[1;33mAPI - v2.0.0\u001b[0m) + + \u001b[1;36mUseful links before you go:\u001b[0m + + - Documentation: \u001b[1;32mhttps://outline.itsnik.de/s/dockstat\u001b[0m + - GitHub (Frontend): \u001b[1;32mhttps://github.com/its4nik/dockstat\u001b[0m + - GitHub (Backend): \u001b[1;32mhttps://github.com/its4nik/dockstatapi\u001b[0m + - API Documentation: \u001b[1;32mhttp://localhost:7000/api-docs\u001b[0m + + \u001b[1;35mSummary:\u001b[0m + + DockStat and DockStatAPI are 2 fully OpenSource projects, DockStatAPI is a simple but extensible API which allows queries via a REST endpoint. + + \u001b[1;31mGoodbye! We hope to see you again soon.\u001b[0m + `); + }); }; export default initializeApp; diff --git a/src/misc/dependencyGraphs/mermaid-all.txt b/src/misc/dependencyGraphs/mermaid-all.txt index 7e77f2c..995f295 100644 --- a/src/misc/dependencyGraphs/mermaid-all.txt +++ b/src/misc/dependencyGraphs/mermaid-all.txt @@ -1,106 +1,125 @@ flowchart LR 0["server.ts"] -subgraph 1["controllers"] -2["highAvailability.ts"] -3["scheduler.ts"] -6["fetchData.ts"] -K["frontendConfiguration.ts"] +subgraph 1["config"] +2["hostsystem.ts"] +B["db.ts"] +1G["swaggerConfig.ts"] end -subgraph 4["config"] -5["db.ts"] -19["swaggerConfig.ts"] +3["os"] +subgraph 4["controllers"] +5["highAvailability.ts"] +9["proxy.ts"] +A["scheduler.ts"] +C["fetchData.ts"] +R["frontendConfiguration.ts"] end -subgraph 7["utils"] -8["containerService.ts"] -9["dockerClient.ts"] -N["connectionChecker.ts"] -P["extractHostData.ts"] -Q["writeOfflineLog.ts"] -subgraph V["notifications"] -W["_notify.ts"] -X["discord.ts"] -Y["_template.ts"] -Z["email.ts"] -10["pushbullet.ts"] -11["pushover.ts"] -12["slack.ts"] -13["telegram.ts"] -14["whatsapp.ts"] +6["util"] +7["init.ts"] +8["process"] +subgraph D["utils"] +E["containerService.ts"] +F["dockerClient.ts"] +U["connectionChecker.ts"] +W["extractHostData.ts"] +X["writeOfflineLog.ts"] +subgraph 12["notifications"] +13["_notify.ts"] +14["discord.ts"] +16["_template.ts"] +17["email.ts"] +18["pushbullet.ts"] +19["pushover.ts"] +1A["slack.ts"] +1B["telegram.ts"] +1C["whatsapp.ts"] end +1F["swaggerDocs.ts"] end -subgraph A["middleware"] -B["authMiddleware.ts"] -C["rateLimiter.ts"] +subgraph G["middleware"] +H["authMiddleware.ts"] +I["checkLock.ts"] +J["rateLimiter.ts"] end -subgraph D["routes"] -subgraph E["auth"] -F["routes.ts"] -end -subgraph G["data"] -H["routes.ts"] +subgraph K["routes"] +subgraph L["auth"] +M["routes.ts"] end -subgraph I["frontendController"] -J["routes.ts"] +subgraph N["data"] +O["routes.ts"] end -subgraph L["getter"] -M["routes.ts"] +subgraph P["frontendController"] +Q["routes.ts"] end -subgraph R["highavailability"] -S["routes.ts"] +subgraph S["getter"] +T["routes.ts"] end -subgraph T["notifications"] -U["routes.ts"] +subgraph Y["highavailability"] +Z["routes.ts"] end -subgraph 15["setter"] -16["routes.ts"] +subgraph 10["notifications"] +11["routes.ts"] end +subgraph 1D["setter"] +1E["routes.ts"] end -O["net"] -subgraph 17["swagger"] -18["swaggerDocs.ts"] end +V["net"] +15["https"] 0-->2 -0-->3 -0-->B -0-->C -0-->F -0-->H -0-->J -0-->M -0-->S -0-->U -0-->16 -0-->18 -3-->5 -3-->6 -6-->5 -6-->8 -8-->9 -H-->5 -J-->K -M-->3 -M-->N -M-->8 -M-->9 -M-->P -M-->Q -N-->O -S-->2 -U-->W -W-->X -W-->Z -W-->10 -W-->11 -W-->12 -W-->13 -W-->14 -X-->Y -Z-->Y -10-->Y -11-->Y -12-->Y -13-->Y -14-->Y -16-->3 -18-->19 +0-->5 +0-->7 +2-->3 +5-->6 +7-->9 +7-->A +7-->H +7-->I +7-->J +7-->M +7-->O +7-->Q +7-->T +7-->Z +7-->11 +7-->1E +7-->1F +7-->8 +A-->B +A-->C +C-->B +C-->E +E-->F +O-->B +Q-->R +T-->A +T-->U +T-->E +T-->F +T-->W +T-->X +U-->V +Z-->5 +11-->13 +13-->14 +13-->17 +13-->18 +13-->19 +13-->1A +13-->1B +13-->1C +14-->16 +14-->15 +17-->16 +18-->16 +18-->15 +19-->16 +19-->15 +1A-->16 +1A-->15 +1B-->16 +1B-->15 +1C-->16 +1C-->15 +1E-->A +1F-->1G diff --git a/src/utils/removeUnusedDeps.sh b/src/utils/removeUnusedDeps.sh index b5b68eb..df72f4b 100755 --- a/src/utils/removeUnusedDeps.sh +++ b/src/utils/removeUnusedDeps.sh @@ -2,7 +2,7 @@ echo "Creating unused dependency list" -TMP="$(npx depcheck --ignores @types/supports-color,ipaddr.js,dependency-cruiser,tsx,@types/bcrypt,@types/express,@types/express-handlebars,@types/node,ts-node --quiet --oneline | tail -n 1 | tr -d '\n')" +TMP="$(npx depcheck --ignores @types/node-fetch,uglify-js,@types/supports-color,ipaddr.js,dependency-cruiser,tsx,@types/bcrypt,@types/express,@types/express-handlebars,@types/node,ts-node --quiet --oneline | tail -n 1 | tr -d '\n')" lines=$(echo "$TMP" | tr -s ' ' '\n' | wc -l) diff --git a/yarn.lock b/yarn.lock index 1418f00..9c80049 100644 --- a/yarn.lock +++ b/yarn.lock @@ -34,39 +34,6 @@ call-me-maybe "^1.0.1" z-schema "^5.0.1" -"@babel/generator@7.18.2": - version "7.18.2" - resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.18.2.tgz" - integrity sha512-W1lG5vUwFvfMd8HVXqdfbuG7RuaSrTCCD8cl8fP8wOivdbtbIg2Db3IWUcgvfxKbbn6ZBGYRW/Zk1MIwK49mgw== - dependencies: - "@babel/types" "^7.18.2" - "@jridgewell/gen-mapping" "^0.3.0" - jsesc "^2.5.1" - -"@babel/helper-string-parser@^7.18.10": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz" - integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== - -"@babel/helper-validator-identifier@^7.18.6": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz" - integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== - -"@babel/parser@7.18.4": - version "7.18.4" - resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.18.4.tgz" - integrity sha512-FDge0dFazETFcxGw/EXzOkN8uJp0PC7Qbm+Pe9T+av2zlBpOgunFHkQPPn+eRuClU73JF+98D531UgayY89tow== - -"@babel/types@^7.18.2", "@babel/types@7.19.0": - version "7.19.0" - resolved "https://registry.npmjs.org/@babel/types/-/types-7.19.0.tgz" - integrity sha512-YuGopBq3ke25BVSiS6fgF49Ul9gH1x70Bcr6bqRLjWCkcX8Hre1/5+z+IiWOIerRMSSEfGZVB9z9kyq7wVs9YA== - dependencies: - "@babel/helper-string-parser" "^7.18.10" - "@babel/helper-validator-identifier" "^7.18.6" - to-fast-properties "^2.0.0" - "@balena/dockerignore@^1.0.2": version "1.0.2" resolved "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz" @@ -103,38 +70,16 @@ resolved "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz" integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== -"@jridgewell/gen-mapping@^0.3.0": - version "0.3.5" - resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz" - integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== - dependencies: - "@jridgewell/set-array" "^1.2.1" - "@jridgewell/sourcemap-codec" "^1.4.10" - "@jridgewell/trace-mapping" "^0.3.24" - -"@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0": +"@jridgewell/resolve-uri@^3.0.3": version "3.1.2" resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz" integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== -"@jridgewell/set-array@^1.2.1": - version "1.2.1" - resolved "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz" - integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== - -"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": +"@jridgewell/sourcemap-codec@^1.4.10": version "1.5.0" resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz" integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== -"@jridgewell/trace-mapping@^0.3.24": - version "0.3.25" - resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz" - integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== - dependencies: - "@jridgewell/resolve-uri" "^3.1.0" - "@jridgewell/sourcemap-codec" "^1.4.14" - "@jridgewell/trace-mapping@0.3.9": version "0.3.9" resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz" @@ -163,27 +108,6 @@ semver "^7.3.5" tar "^6.1.11" -"@nodelib/fs.scandir@2.1.5": - version "2.1.5" - resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" - integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== - dependencies: - "@nodelib/fs.stat" "2.0.5" - run-parallel "^1.1.9" - -"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5": - version "2.0.5" - resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" - integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== - -"@nodelib/fs.walk@^1.2.3": - version "1.2.8" - resolved "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz" - integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== - dependencies: - "@nodelib/fs.scandir" "2.1.5" - fastq "^1.6.0" - "@npmcli/fs@^1.0.0": version "1.1.1" resolved "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz" @@ -493,7 +417,7 @@ ansi-regex@^6.0.1: resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz" integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== -ansi-styles@^4.0.0, ansi-styles@^4.1.0: +ansi-styles@^4.1.0: version "4.3.0" resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== @@ -544,11 +468,6 @@ array-flatten@1.1.1: resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz" integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== -array-union@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== - asn1@^0.2.6: version "0.2.6" resolved "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz" @@ -566,11 +485,6 @@ asynckit@^0.4.0: resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== -at-least-node@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz" - integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== - balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" @@ -643,7 +557,7 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" -braces@^3.0.3, braces@~3.0.2: +braces@~3.0.2: version "3.0.3" resolved "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz" integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== @@ -723,14 +637,6 @@ chalk@^4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^4.1.2: - version "4.1.2" - resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - chalk@^5.3.0: version "5.3.0" resolved "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz" @@ -785,15 +691,6 @@ cli-spinners@^2.9.2: resolved "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz" integrity sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg== -cliui@^7.0.2: - version "7.0.4" - resolved "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz" - integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.0" - wrap-ansi "^7.0.0" - color-convert@^1.9.3: version "1.9.3" resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz" @@ -901,11 +798,6 @@ cookie@0.7.1: resolved "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz" integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== -core-util-is@~1.0.0: - version "1.0.3" - resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz" - integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== - cors@^2.8.5: version "2.8.5" resolved "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz" @@ -1025,13 +917,6 @@ diff@^4.0.1: resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== -dir-glob@^3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz" - integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== - dependencies: - path-type "^4.0.0" - docker-modem@^5.0.3: version "5.0.3" resolved "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.3.tgz" @@ -1169,11 +1054,6 @@ esbuild@~0.23.0: "@esbuild/win32-ia32" "0.23.1" "@esbuild/win32-x64" "0.23.1" -escalade@^3.1.1: - version "3.2.0" - resolved "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz" - integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== - escape-html@~1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" @@ -1241,29 +1121,11 @@ fast-deep-equal@^3.1.3: resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-glob@^3.2.9: - version "3.3.2" - resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz" - integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.4" - fast-uri@^3.0.1: version "3.0.3" resolved "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz" integrity sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw== -fastq@^1.6.0: - version "1.17.1" - resolved "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz" - integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== - dependencies: - reusify "^1.0.4" - fecha@^4.2.0: version "4.2.3" resolved "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz" @@ -1333,29 +1195,11 @@ fresh@0.5.2: resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz" integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== -from2@^2.3.0: - version "2.3.0" - resolved "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz" - integrity sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g== - dependencies: - inherits "^2.0.1" - readable-stream "^2.0.0" - fs-constants@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== -fs-extra@^9.1.0: - version "9.1.0" - resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz" - integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== - dependencies: - at-least-node "^1.0.0" - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - fs-minipass@^2.0.0: version "2.1.0" resolved "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz" @@ -1402,11 +1246,6 @@ gauge@^4.0.3: strip-ansi "^6.0.1" wide-align "^1.1.5" -get-caller-file@^2.0.5: - version "2.0.5" - resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - get-east-asian-width@^1.0.0: version "1.3.0" resolved "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz" @@ -1438,7 +1277,7 @@ github-from-package@0.0.0: resolved "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz" integrity sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw== -glob-parent@^5.1.2, glob-parent@~5.1.2: +glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== @@ -1476,24 +1315,12 @@ global-directory@^4.0.1: dependencies: ini "4.1.1" -globby@^11.1.0: - version "11.1.0" - resolved "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz" - integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== - dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.2.9" - ignore "^5.2.0" - merge2 "^1.4.1" - slash "^3.0.0" - gopd@^1.0.1, gopd@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz" integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== -graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6: +graceful-fs@^4.2.4, graceful-fs@^4.2.6: version "4.2.11" resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -1525,11 +1352,6 @@ has-unicode@^2.0.1: resolved "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz" integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== -has@^1.0.3: - version "1.0.4" - resolved "https://registry.npmjs.org/has/-/has-1.0.4.tgz" - integrity sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ== - hasown@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz" @@ -1606,11 +1428,6 @@ ignore-by-default@^1.0.1: resolved "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz" integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== -ignore@^5.2.0: - version "5.3.2" - resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz" - integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== - ignore@^6.0.2: version "6.0.2" resolved "https://registry.npmjs.org/ignore/-/ignore-6.0.2.tgz" @@ -1639,7 +1456,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3, inherits@2, inherits@2.0.4: +inherits@^2.0.3, inherits@^2.0.4, inherits@2, inherits@2.0.4: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -1659,14 +1476,6 @@ interpret@^3.1.1: resolved "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz" integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ== -into-stream@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/into-stream/-/into-stream-6.0.0.tgz" - integrity sha512-XHbaOAvP+uFKUFsOgoNPRjLkwB+I22JFPFe5OjTkQ0nwgj6+pSjb4NmB6VMxaPshLiOf+zcpOCBQuLwC1KHhZA== - dependencies: - from2 "^2.3.0" - p-is-promise "^3.0.0" - ip-address@^9.0.5: version "9.0.5" resolved "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz" @@ -1704,13 +1513,6 @@ is-core-module@^2.13.0: dependencies: hasown "^2.0.2" -is-core-module@2.9.0: - version "2.9.0" - resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz" - integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A== - dependencies: - has "^1.0.3" - is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" @@ -1771,11 +1573,6 @@ is-unicode-supported@^2.0.0: resolved "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz" integrity sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ== -isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" - integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== - isexe@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" @@ -1793,11 +1590,6 @@ jsbn@1.1.0: resolved "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz" integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A== -jsesc@^2.5.1: - version "2.5.2" - resolved "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz" - integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== - json-schema-traverse@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz" @@ -1808,15 +1600,6 @@ json5@^2.2.2, json5@^2.2.3: resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== -jsonfile@^6.0.1: - version "6.1.0" - resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz" - integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== - dependencies: - universalify "^2.0.0" - optionalDependencies: - graceful-fs "^4.1.6" - kleur@^3.0.3: version "3.0.3" resolved "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz" @@ -1920,24 +1703,11 @@ merge-descriptors@1.0.3: resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz" integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== -merge2@^1.3.0, merge2@^1.4.1: - version "1.4.1" - resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" - integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== - methods@~1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== -micromatch@^4.0.4: - version "4.0.8" - resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz" - integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== - dependencies: - braces "^3.0.3" - picomatch "^2.3.1" - mime-db@1.52.0: version "1.52.0" resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" @@ -2056,14 +1826,6 @@ ms@2.0.0: resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== -multistream@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/multistream/-/multistream-4.1.0.tgz" - integrity sha512-J1XDiAmmNpRCBfIWJv+n0ymC4ABcf/Pl+5YvC5B/D2f/2+8PtHvCNxMPKiQcZyi922Hq69J2YOpb1pTywfifyw== - dependencies: - once "^1.4.0" - readable-stream "^3.6.0" - nan@^2.19.0, nan@^2.20.0: version "2.22.0" resolved "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz" @@ -2101,13 +1863,6 @@ node-domexception@^1.0.0: resolved "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz" integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== -node-fetch@^2.6.6: - version "2.7.0" - resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz" - integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== - dependencies: - whatwg-url "^5.0.0" - node-fetch@^2.6.7: version "2.7.0" resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz" @@ -2251,11 +2006,6 @@ ora@^8.1.1: string-width "^7.2.0" strip-ansi "^7.1.0" -p-is-promise@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/p-is-promise/-/p-is-promise-3.0.0.tgz" - integrity sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ== - p-map@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz" @@ -2283,11 +2033,6 @@ path-to-regexp@0.1.12: resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz" integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ== -path-type@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== - picocolors@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz" @@ -2303,50 +2048,11 @@ picomatch@^2.2.1: resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -picomatch@^2.3.1: - version "2.3.1" - resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== - picomatch@^4.0.2: version "4.0.2" resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz" integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg== -pkg-fetch@3.4.2: - version "3.4.2" - resolved "https://registry.npmjs.org/pkg-fetch/-/pkg-fetch-3.4.2.tgz" - integrity sha512-0+uijmzYcnhC0hStDjm/cl2VYdrmVVBpe7Q8k9YBojxmR5tG8mvR9/nooQq3QSXiQqORDVOTY3XqMEqJVIzkHA== - dependencies: - chalk "^4.1.2" - fs-extra "^9.1.0" - https-proxy-agent "^5.0.0" - node-fetch "^2.6.6" - progress "^2.0.3" - semver "^7.3.5" - tar-fs "^2.1.1" - yargs "^16.2.0" - -pkg@^5.8.1: - version "5.8.1" - resolved "https://registry.npmjs.org/pkg/-/pkg-5.8.1.tgz" - integrity sha512-CjBWtFStCfIiT4Bde9QpJy0KeH19jCfwZRJqHFDFXfhUklCx8JoFmMj3wgnEYIwGmZVNkhsStPHEOnrtrQhEXA== - dependencies: - "@babel/generator" "7.18.2" - "@babel/parser" "7.18.4" - "@babel/types" "7.19.0" - chalk "^4.1.2" - fs-extra "^9.1.0" - globby "^11.1.0" - into-stream "^6.0.0" - is-core-module "2.9.0" - minimist "^1.2.6" - multistream "^4.1.0" - pkg-fetch "3.4.2" - prebuild-install "7.1.1" - resolve "^1.22.0" - stream-meter "^1.0.4" - playwright-core@1.49.0: version "1.49.0" resolved "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0.tgz" @@ -2379,34 +2085,6 @@ prebuild-install@^7.1.1: tar-fs "^2.0.0" tunnel-agent "^0.6.0" -prebuild-install@7.1.1: - version "7.1.1" - resolved "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz" - integrity sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw== - dependencies: - detect-libc "^2.0.0" - expand-template "^2.0.3" - github-from-package "0.0.0" - minimist "^1.2.3" - mkdirp-classic "^0.5.3" - napi-build-utils "^1.0.1" - node-abi "^3.3.0" - pump "^3.0.0" - rc "^1.2.7" - simple-get "^4.0.0" - tar-fs "^2.0.0" - tunnel-agent "^0.6.0" - -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - -progress@^2.0.3: - version "2.0.3" - resolved "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz" - integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== - promise-inflight@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz" @@ -2456,11 +2134,6 @@ qs@6.13.0: dependencies: side-channel "^1.0.6" -queue-microtask@^1.2.2: - version "1.2.3" - resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" - integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== - range-parser@~1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" @@ -2486,32 +2159,6 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -readable-stream@^2.0.0: - version "2.3.8" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz" - integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readable-stream@^2.1.4: - version "2.3.8" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz" - integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0, readable-stream@^3.6.2: version "3.6.2" resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz" @@ -2545,11 +2192,6 @@ regexp-tree@~0.1.1: resolved "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz" integrity sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA== -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz" - integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== - require-from-string@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz" @@ -2560,7 +2202,7 @@ resolve-pkg-maps@^1.0.0: resolved "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz" integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== -resolve@^1.20.0, resolve@^1.22.0: +resolve@^1.20.0: version "1.22.8" resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz" integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== @@ -2582,11 +2224,6 @@ retry@^0.12.0: resolved "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz" integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== -reusify@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" - integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== - rimraf@^3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" @@ -2594,23 +2231,11 @@ rimraf@^3.0.2: dependencies: glob "^7.1.3" -run-parallel@^1.1.9: - version "1.2.0" - resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz" - integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== - dependencies: - queue-microtask "^1.2.2" - safe-buffer@^5.0.1, safe-buffer@~5.2.0, safe-buffer@5.2.1: version "5.2.1" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - safe-regex@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz" @@ -2742,11 +2367,6 @@ sisteransi@^1.0.5: resolved "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz" integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== -slash@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" - integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== - smart-buffer@^4.2.0: version "4.2.0" resolved "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz" @@ -2824,13 +2444,6 @@ stdin-discarder@^0.2.2: resolved "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz" integrity sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ== -stream-meter@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/stream-meter/-/stream-meter-1.0.4.tgz" - integrity sha512-4sOEtrbgFotXwnEuzzsQBYEV1elAeFSO8rSGeTwabuX1RRn/kEq9JVH7I0MRBhKVRR0sJkr0M0QCH7yOLf9fhQ== - dependencies: - readable-stream "^2.1.4" - string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" @@ -2838,14 +2451,7 @@ string_decoder@^1.1.1: dependencies: safe-buffer "~5.2.0" -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -2863,7 +2469,7 @@ string-width@^7.2.0: get-east-asian-width "^1.0.0" strip-ansi "^7.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -2954,17 +2560,7 @@ tar-fs@^2.0.0, tar-fs@~2.0.1: pump "^3.0.0" tar-stream "^2.0.0" -tar-fs@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz" - integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== - dependencies: - chownr "^1.1.1" - mkdirp-classic "^0.5.2" - pump "^3.0.0" - tar-stream "^2.1.4" - -tar-stream@^2.0.0, tar-stream@^2.1.4: +tar-stream@^2.0.0: version "2.2.0" resolved "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz" integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== @@ -2997,11 +2593,6 @@ text-hex@1.0.x: resolved "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz" integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg== -to-fast-properties@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz" - integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== - to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" @@ -3136,17 +2727,12 @@ unique-slug@^2.0.0: dependencies: imurmurhash "^0.1.4" -universalify@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz" - integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== - unpipe@~1.0.0, unpipe@1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== -util-deprecate@^1.0.1, util-deprecate@~1.0.1: +util-deprecate@^1.0.1: version "1.0.2" resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== @@ -3234,25 +2820,11 @@ winston@^3.15.0: triple-beam "^1.3.0" winston-transport "^4.9.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrappy@1: version "1.0.2" resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== -y18n@^5.0.5: - version "5.0.8" - resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz" - integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== - yallist@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" @@ -3263,24 +2835,6 @@ yaml@2.0.0-1: resolved "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz" integrity sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ== -yargs-parser@^20.2.2: - version "20.2.9" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz" - integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== - -yargs@^16.2.0: - version "16.2.0" - resolved "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz" - integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== - dependencies: - cliui "^7.0.2" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.0" - y18n "^5.0.5" - yargs-parser "^20.2.2" - yn@3.1.1: version "3.1.1" resolved "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz" From aeaef0a4c1c351a1bac751087f272e78e4870958 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sat, 21 Dec 2024 13:44:56 +0100 Subject: [PATCH 021/324] Fix: remove verbose flag in workflow Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 87792b0..df4f276 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,7 +20,7 @@ RUN apk update && \ COPY tsconfig.json environment.d.ts package*.json tsconfig.json yarn.lock ./ -RUN npm install --verbose +RUN npm install COPY ./src ./src RUN npm run build:mini From 0a9f32600694e7843ea7733db0e98a215b6b9dda Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sat, 21 Dec 2024 13:46:57 +0100 Subject: [PATCH 022/324] Fix: Fixing typo in ReadMe Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ae34767..e6a17bb 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ With this new release a couple of extra features (compared to v1) are going to b - Multi-arch docker builds (using buildx github action) - Advanced security through middlewares: rate-limiting and authentication - Multi Arch Docker builds through docker buildx -- High Availability using single master and ulimited worker nodes! +- High Availability using single master and unlimited worker nodes! # 🔗 DockStatAPI v2 Documentation From 4af32e873635f9a9d0922544cb678653fd61fde3 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sat, 21 Dec 2024 13:48:32 +0100 Subject: [PATCH 023/324] Fix: CodeQL Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- src/controllers/frontendConfiguration.ts | 32 ++++++++++++------------ 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/controllers/frontendConfiguration.ts b/src/controllers/frontendConfiguration.ts index 6a5a691..e83eaae 100644 --- a/src/controllers/frontendConfiguration.ts +++ b/src/controllers/frontendConfiguration.ts @@ -185,22 +185,22 @@ async function setIcon(containerName: string, icon: string, custom: boolean) { ); if (custom === true) { - if (containerIndex !== -1) { - data[containerIndex].icon = `custom/${icon}`; - await saveData(data); - } else { - data.push({ name: containerName, icon: `custom/${icon}` }); - await saveData(data); - } - } else { - if (containerIndex !== -1) { - data[containerIndex].icon = `${icon}`; - await saveData(data); - } else { - data.push({ name: containerName, icon: `${icon}` }); - await saveData(data); - } - } + if (containerIndex !== -1) { + data[containerIndex].icon = `custom/${icon}`; + await saveData(data); + } else { + data.push({ name: containerName, icon: `custom/${icon}` }); + await saveData(data); + } + } + else if (containerIndex !== -1) { + data[containerIndex].icon = `${icon}`; + await saveData(data); + } + else { + data.push({ name: containerName, icon: `${icon}` }); + await saveData(data); + } } catch (error: any) { logger.error(error); throw new Error(error); From 00e9ffe396bc85f3709575982a56ff0635f9d025 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sat, 21 Dec 2024 13:49:15 +0100 Subject: [PATCH 024/324] Fix: Use object destruction Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- src/routes/getter/routes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/getter/routes.ts b/src/routes/getter/routes.ts index 0f9883f..c559e63 100644 --- a/src/routes/getter/routes.ts +++ b/src/routes/getter/routes.ts @@ -134,7 +134,7 @@ router.get("/system", (req: Request, res: Response) => { * description: Error message detailing the issue encountered. */ router.get("/host/:hostName/stats", async (req: Request, res: Response) => { - const hostName = req.params.hostName; + const {hostName} = req.params; logger.info(`Fetching stats for host: ${hostName}`); if (process.env.OFFLINE === "true") { logger.info("Fetching offline Host Stats"); From 47ff28c6692eba5e5f62ffa57c2a5e96c454a285 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sat, 21 Dec 2024 13:49:55 +0100 Subject: [PATCH 025/324] Fix: CodeQL (braces for if clauses) Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- src/utils/notifications/_template.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/notifications/_template.ts b/src/utils/notifications/_template.ts index ecc327e..2b6e3a4 100644 --- a/src/utils/notifications/_template.ts +++ b/src/utils/notifications/_template.ts @@ -44,7 +44,7 @@ function renderTemplate(containerId: string) { let containerData = null; for (const host in containers) { containerData = containers[host].find((c: any) => c.id === containerId); - if (containerData) break; + if (containerData) { } if (!containerData) { From 5404206ea5e649a6ac4ebeed828a5d7ce2783a94 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sat, 21 Dec 2024 13:52:12 +0100 Subject: [PATCH 026/324] Chore: Update Dockerfile --- Dockerfile | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index df4f276..7bce202 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ LABEL description="API for DockStat" LABEL license="BSD-3-Clause license" LABEL repository="https://github.com/its4nik/dockstatapi" LABEL documentation="https://github.com/its4nik/dockstatapi" -LABEL org.opencontainers.image.description "The DockSatAPI is a free and OpenSource backend for gathering container statistics across hosts" +LABEL org.opencontainers.image.description="The DockSatAPI is a free and OpenSource backend for gathering container statistics across hosts" LABEL org.opencontainers.image.licenses="BSD-3-Clause license" LABEL org.opencontainers.image.source="https://github.com/its4nik/dockstatapi" @@ -48,8 +48,6 @@ RUN node src/config/db.js # Stage 3: Production stage FROM alpine AS production -ARG RUNNING_IN_DOCKER=true -RUN apk add --update bash nodejs WORKDIR /api From 7e2ca2049c43bea91a9b3a944d556e11298d156b Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sat, 21 Dec 2024 13:55:10 +0100 Subject: [PATCH 027/324] Chore: Update _template.ts --- src/utils/notifications/_template.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils/notifications/_template.ts b/src/utils/notifications/_template.ts index 2b6e3a4..8843994 100644 --- a/src/utils/notifications/_template.ts +++ b/src/utils/notifications/_template.ts @@ -45,6 +45,7 @@ function renderTemplate(containerId: string) { for (const host in containers) { containerData = containers[host].find((c: any) => c.id === containerId); if (containerData) { + break; } if (!containerData) { From 71b8e2fa1a66a59e75561ab21dbad8bbd5187c08 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sat, 21 Dec 2024 13:56:35 +0100 Subject: [PATCH 028/324] Update _template.ts --- src/utils/notifications/_template.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/notifications/_template.ts b/src/utils/notifications/_template.ts index 8843994..bbec96f 100644 --- a/src/utils/notifications/_template.ts +++ b/src/utils/notifications/_template.ts @@ -45,7 +45,7 @@ function renderTemplate(containerId: string) { for (const host in containers) { containerData = containers[host].find((c: any) => c.id === containerId); if (containerData) { - break; + break(); } if (!containerData) { From b0c266cf6df94413f1f71a6f052c56f55547e19b Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 23 Dec 2024 08:30:49 +0100 Subject: [PATCH 029/324] Chore: Add npm run docker(:build) commands --- .gitignore | 1 + docker-compose.yaml | 23 +++++++++++++++++++++++ package.json | 14 ++++++++------ src/data/usePassword.txt | 2 +- 4 files changed, 33 insertions(+), 7 deletions(-) create mode 100644 docker-compose.yaml diff --git a/.gitignore b/.gitignore index 43ddf88..fbbbb21 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ src/data/user.conf src/data/password.json src/data/ha.lock +docker .test* # Created by https://www.toptal.com/developers/gitignore/api/node ### Node ### diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..721b8cc --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,23 @@ +services: + master: + container_name: master + environment: + - NODE_ENV=development + - HA_MASTER=true + - HA_MASTER_IP=localhost:9876 + - HA_NODE=localhost:6789 + - HA_UNSAFE=true + volumes: + - ./docker/master:/api/src/data + ports: + - 9876:9876 + image: dockstatapi:local + slave: + container_name: slave + environment: + - NODE_ENV=development + volumes: + - ./docker/slave:/api/src/data + ports: + - 6789:6789 + image: dockstatapi:local diff --git a/package.json b/package.json index 5e85ece..9517b02 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dockstatapi", - "version": "2", + "version": "2.0.0", "description": "API for docker hosts using dockerode", "main": "src/server.ts", "scripts": { @@ -12,16 +12,14 @@ "dep:remove": "bash ./src/utils/removeUnusedDeps.sh && bash ./src/utils/createDependencyGraph.sh", "build": "npx tsc", "build:mini": "npx tsc && bash ./src/misc/minifyDist.sh --build-only", - "mini": "bash ./src/misc/minifyDist.sh" + "mini": "bash ./src/misc/minifyDist.sh", + "docker": "sudo docker compose up", + "docker:build": "sudo docker build . -t \"dockstatapi:local\" && sudo docker compose up" }, "keywords": [], "author": "Its4Nik", "license": "BSD 3-Clause License", "dependencies": { - "@types/dockerode": "^3.3.31", - "@types/supports-color": "^8.1.3", - "@types/swagger-jsdoc": "^6.0.4", - "@types/swagger-ui-express": "^4.1.7", "bcrypt": "^5.1.1", "chokidar": "^4.0.1", "cors": "^2.8.5", @@ -38,6 +36,10 @@ "winston": "^3.15.0" }, "devDependencies": { + "@types/dockerode": "^3.3.31", + "@types/supports-color": "^8.1.3", + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.7", "@playwright/test": "^1.49.0", "@types/bcrypt": "^5.0.2", "@types/cors": "^2.8.17", diff --git a/src/data/usePassword.txt b/src/data/usePassword.txt index 02e4a84..c508d53 100644 --- a/src/data/usePassword.txt +++ b/src/data/usePassword.txt @@ -1 +1 @@ -false \ No newline at end of file +false From 72d4c12e884e3e1c2fc73cb6308c1be00b729379 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 23 Dec 2024 08:35:06 +0100 Subject: [PATCH 030/324] Fix: Fixing 'break' syntax, in _template.ts --- Dockerfile | 2 +- src/utils/notifications/_template.ts | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Dockerfile b/Dockerfile index 7bce202..59a59ca 100644 --- a/Dockerfile +++ b/Dockerfile @@ -38,7 +38,7 @@ WORKDIR /build RUN mkdir -p /build/src/data COPY tsconfig.json environment.d.ts package*.json tsconfig.json yarn.lock ./ -RUN npm install --omit=dev --verbose +RUN npm install --omit=dev COPY --from=builder /build/dist/* /build/src COPY --from=builder /build/src/misc/entrypoint.sh /build/entrypoint.sh diff --git a/src/utils/notifications/_template.ts b/src/utils/notifications/_template.ts index bbec96f..551da82 100644 --- a/src/utils/notifications/_template.ts +++ b/src/utils/notifications/_template.ts @@ -1,13 +1,14 @@ import fs from "fs"; import logger from "../logger"; + const templatePath: string = "./src/data/template.json"; const containersPath: string = "./src/data/states.json"; interface Template { - "text": string + text: string; } -function getTemplate() { +function getTemplate(): Template | null { try { const data = fs.readFileSync(templatePath, "utf8"); return JSON.parse(data); @@ -17,11 +18,11 @@ function getTemplate() { } } -function setTemplate(newTemplate: string) { +function setTemplate(newTemplate: string): void { try { fs.writeFileSync( templatePath, - JSON.stringify(newTemplate, null, 2), + JSON.stringify({ text: newTemplate }, null, 2), "utf8", ); logger.debug("Template updated successfully"); @@ -30,8 +31,8 @@ function setTemplate(newTemplate: string) { } } -function renderTemplate(containerId: string) { - const template: Template = getTemplate(); +function renderTemplate(containerId: string): string | null { + const template = getTemplate(); if (!template) { logger.error("Template is missing or not a string"); return null; @@ -41,11 +42,12 @@ function renderTemplate(containerId: string) { const data = fs.readFileSync(containersPath, "utf8"); const containers = JSON.parse(data); - let containerData = null; + let containerData: Record | null = null; for (const host in containers) { containerData = containers[host].find((c: any) => c.id === containerId); if (containerData) { - break(); + break; + } } if (!containerData) { @@ -65,5 +67,4 @@ function renderTemplate(containerId: string) { } } - export { getTemplate, setTemplate, renderTemplate }; From 84cfbf39ee4847d5cce931d15930354d63849da2 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 23 Dec 2024 08:45:58 +0100 Subject: [PATCH 031/324] Feat: Add CI/CD Badges to ReadMe --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index e6a17bb..6fd1f00 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ # DockStatAPI v2 ![Dockstat Logo](.github/DockStat.png) +*Pipelines:* +Main: [![GitHubCI pipeline status badge](https://github.com/its4nik/dokstatapi/workflows/build-main/badge.svg?branch=main)](https://github.com/its4nik/dokstatapi/commits/main) +Dev : [![GitHubCI pipeline status badge](https://github.com/its4nik/dokstatapi/workflows/build-dev/badge.svg?branch=dev)](https://github.com/its4nik/dokstatapi/commits/dev) + This specific branch contains the currently WIP **DockStatAPI-v2**, this update will bring major breaking changes so please be careful. With this new release a couple of extra features (compared to v1) are going to be available. From 1e16f950e452ca0bd6135b2b19abe75ca03e678c Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 23 Dec 2024 08:47:50 +0100 Subject: [PATCH 032/324] Fix: Change badges --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6fd1f00..034c009 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ ![Dockstat Logo](.github/DockStat.png) *Pipelines:* -Main: [![GitHubCI pipeline status badge](https://github.com/its4nik/dokstatapi/workflows/build-main/badge.svg?branch=main)](https://github.com/its4nik/dokstatapi/commits/main) -Dev : [![GitHubCI pipeline status badge](https://github.com/its4nik/dokstatapi/workflows/build-dev/badge.svg?branch=dev)](https://github.com/its4nik/dokstatapi/commits/dev) +[![Docker Image CI](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-image.yml/badge.svg?branch=main)](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-image.yml) +[![Build dockstatapi:nightly](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-dev.yaml/badge.svg?branch=dev)](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-dev.yaml) This specific branch contains the currently WIP **DockStatAPI-v2**, this update will bring major breaking changes so please be careful. With this new release a couple of extra features (compared to v1) are going to be available. From f2322bc907a4495209b8ef881f38019261eaed6b Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 23 Dec 2024 08:48:20 +0100 Subject: [PATCH 033/324] Fix: Update README.md with newlines between the badges --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 034c009..f602f3b 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # DockStatAPI v2 ![Dockstat Logo](.github/DockStat.png) -*Pipelines:* -[![Docker Image CI](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-image.yml/badge.svg?branch=main)](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-image.yml) -[![Build dockstatapi:nightly](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-dev.yaml/badge.svg?branch=dev)](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-dev.yaml) +*Pipelines:*
+[![Docker Image CI](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-image.yml/badge.svg?branch=main)](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-image.yml)
+[![Build dockstatapi:nightly](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-dev.yaml/badge.svg?branch=dev)](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-dev.yaml)
This specific branch contains the currently WIP **DockStatAPI-v2**, this update will bring major breaking changes so please be careful. With this new release a couple of extra features (compared to v1) are going to be available. From 1f0c5d652199121745ddba92ad825336bef50375 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 23 Dec 2024 13:23:29 +0100 Subject: [PATCH 034/324] Fix: Needs bash and curl for healthcheck / entrypoint --- Dockerfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Dockerfile b/Dockerfile index 59a59ca..5e49bbd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -49,6 +49,10 @@ RUN node src/config/db.js # Stage 3: Production stage FROM alpine AS production +RUN apk add --update bash curl +HEALTHCHECK --interval=5m --timeout=3s \ + curl -f http://localhost:9876/api/status || exit 1 + WORKDIR /api COPY --from=main /build /api From 0de6ba9c6fe53c5669b3553a0e8a539cb75d1040 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 23 Dec 2024 13:24:45 +0100 Subject: [PATCH 035/324] Fix: Add 'CMD' to HEALTHCHECK --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 5e49bbd..f463522 100644 --- a/Dockerfile +++ b/Dockerfile @@ -51,7 +51,7 @@ FROM alpine AS production RUN apk add --update bash curl HEALTHCHECK --interval=5m --timeout=3s \ - curl -f http://localhost:9876/api/status || exit 1 + CMD curl -f http://localhost:9876/api/status || exit 1 WORKDIR /api From 522b2d093ba86a88d286cbd91376fa7d1d1d8c34 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 23 Dec 2024 23:27:00 +0100 Subject: [PATCH 036/324] Fix: Adding bash/nodejs binaries to last docker stage --- .gitignore | 1 + Dockerfile | 6 ++--- docker-compose.yaml | 42 +++++++++++++++++++++++++++-- package-lock.json | 32 +++++++++++++++++----- package.json | 6 +++-- src/config/loggerConfig.ts | 11 ++++++++ src/data/frontendConfiguration.json | 8 ++++++ src/data/usePassword.txt | 2 +- src/init.ts | 23 ---------------- src/server.ts | 6 +++-- 10 files changed, 98 insertions(+), 39 deletions(-) create mode 100644 src/data/frontendConfiguration.json diff --git a/.gitignore b/.gitignore index fbbbb21..c7f5c64 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ src/data/states.json src/data/user.conf src/data/password.json src/data/ha.lock +src/data/frontendConfiguration.json docker .test* diff --git a/Dockerfile b/Dockerfile index f463522..78ee53b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM node:alpine AS builder LABEL maintainer="https://github.com/its4nik" -LABEL version="2" +LABEL version="2.0.0" LABEL description="API for DockStat" LABEL license="BSD-3-Clause license" LABEL repository="https://github.com/its4nik/dockstatapi" @@ -49,9 +49,9 @@ RUN node src/config/db.js # Stage 3: Production stage FROM alpine AS production -RUN apk add --update bash curl +RUN apk add --update bash curl nodejs HEALTHCHECK --interval=5m --timeout=3s \ - CMD curl -f http://localhost:9876/api/status || exit 1 + CMD curl -f http://localhost:9876/api/status || exit 1 WORKDIR /api diff --git a/docker-compose.yaml b/docker-compose.yaml index 721b8cc..3158657 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -4,8 +4,8 @@ services: environment: - NODE_ENV=development - HA_MASTER=true - - HA_MASTER_IP=localhost:9876 - - HA_NODE=localhost:6789 + - HA_MASTER_IP=127.0.0.1:9876 + - HA_NODE=127.0.0.1:6789 - HA_UNSAFE=true volumes: - ./docker/master:/api/src/data @@ -21,3 +21,41 @@ services: ports: - 6789:6789 image: dockstatapi:local + + test-socket-proxy: + image: lscr.io/linuxserver/socket-proxy:latest + container_name: socket-proxy + environment: + - ALLOW_START=1 #optional + - ALLOW_STOP=1 #optional + - ALLOW_RESTARTS=1 #optional + - AUTH=0 #optional + - BUILD=0 #optional + - COMMIT=0 #optional + - CONFIGS=0 #optional + - CONTAINERS=1 #optional + - DISABLE_IPV6=0 #optional + - DISTRIBUTION=0 #optional + - EVENTS=1 #optional + - EXEC=0 #optional + - IMAGES=0 #optional + - INFO=1 #optional + - NETWORKS=1 #optional + - NODES=1 #optional + - PING=1 #optional + - POST=0 #optional + - PLUGINS=0 #optional + - SECRETS=0 #optional + - SERVICES=0 #optional + - SESSION=0 #optional + - SWARM=0 #optional + - SYSTEM=0 #optional + - TASKS=0 #optional + - VERSION=1 #optional + - VOLUMES=0 #optional + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + restart: unless-stopped + read_only: true + tmpfs: + - /run diff --git a/package-lock.json b/package-lock.json index dcd2ac0..27899c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,14 @@ { "name": "dockstatapi", - "version": "2", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dockstatapi", - "version": "2", + "version": "2.0.0", "license": "BSD 3-Clause License", "dependencies": { - "@types/dockerode": "^3.3.31", - "@types/supports-color": "^8.1.3", - "@types/swagger-jsdoc": "^6.0.4", - "@types/swagger-ui-express": "^4.1.7", "bcrypt": "^5.1.1", "chokidar": "^4.0.1", "cors": "^2.8.5", @@ -32,11 +28,15 @@ "@playwright/test": "^1.49.0", "@types/bcrypt": "^5.0.2", "@types/cors": "^2.8.17", + "@types/dockerode": "^3.3.31", "@types/express": "^5.0.0", "@types/express-handlebars": "^5.3.1", "@types/node": "^22.9.0", "@types/node-fetch": "^2.6.12", "@types/nodemailer": "^6.4.17", + "@types/supports-color": "^8.1.3", + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.7", "dependency-cruiser": "^16.5.0", "nodemon": "^3.1.7", "ora": "^8.1.1", @@ -721,6 +721,7 @@ "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, "license": "MIT", "dependencies": { "@types/connect": "*", @@ -731,6 +732,7 @@ "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -750,6 +752,7 @@ "version": "3.0.6", "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz", "integrity": "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -760,6 +763,7 @@ "version": "3.3.32", "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.32.tgz", "integrity": "sha512-xxcG0g5AWKtNyh7I7wswLdFvym4Mlqks5ZlKzxEUrGHS0r0PUOfxm2T0mspwu10mHQqu3Ck3MI3V2HqvLWE1fg==", + "dev": true, "license": "MIT", "dependencies": { "@types/docker-modem": "*", @@ -771,6 +775,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", @@ -790,6 +795,7 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.2.tgz", "integrity": "sha512-vluaspfvWEtE4vcSDlKRNer52DvOGrB2xv6diXy6UKyKW0lqZiWHGNApSyxOv+8DE5Z27IzVvE7hNkxg7EXIcg==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -802,6 +808,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true, "license": "MIT" }, "node_modules/@types/json-schema": { @@ -814,12 +821,14 @@ "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, "license": "MIT" }, "node_modules/@types/node": { "version": "22.10.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", + "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.20.0" @@ -850,18 +859,21 @@ "version": "6.9.17", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz", "integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==", + "dev": true, "license": "MIT" }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, "license": "MIT" }, "node_modules/@types/send": { "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, "license": "MIT", "dependencies": { "@types/mime": "^1", @@ -872,6 +884,7 @@ "version": "1.15.7", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, "license": "MIT", "dependencies": { "@types/http-errors": "*", @@ -883,6 +896,7 @@ "version": "1.15.1", "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.1.tgz", "integrity": "sha512-ZIbEqKAsi5gj35y4P4vkJYly642wIbY6PqoN0xiyQGshKUGXR9WQjF/iF9mXBQ8uBKy3ezfsCkcoHKhd0BzuDA==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "^18.11.18" @@ -892,6 +906,7 @@ "version": "18.19.67", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.67.tgz", "integrity": "sha512-wI8uHusga+0ZugNp0Ol/3BqQfEcCCNfojtO6Oou9iVNGPTL6QNSdnUdqq85fRgIorLhLMuPIKpsN98QE9Nh+KQ==", + "dev": true, "license": "MIT", "dependencies": { "undici-types": "~5.26.4" @@ -901,24 +916,28 @@ "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, "license": "MIT" }, "node_modules/@types/supports-color": { "version": "8.1.3", "resolved": "https://registry.npmjs.org/@types/supports-color/-/supports-color-8.1.3.tgz", "integrity": "sha512-Hy6UMpxhE3j1tLpl27exp1XqHD7n8chAiNPzWfz16LPZoMMoSc4dzLl6w9qijkEb/r5O1ozdu1CWGA2L83ZeZg==", + "dev": true, "license": "MIT" }, "node_modules/@types/swagger-jsdoc": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.4.tgz", "integrity": "sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ==", + "dev": true, "license": "MIT" }, "node_modules/@types/swagger-ui-express": { "version": "4.1.7", "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.7.tgz", "integrity": "sha512-ovLM9dNincXkzH4YwyYpll75vhzPBlWx6La89wwvYH7mHjVpf0X0K/vR/aUM7SRxmr5tt9z7E5XJcjQ46q+S3g==", + "dev": true, "license": "MIT", "dependencies": { "@types/express": "*", @@ -5088,6 +5107,7 @@ "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, "license": "MIT" }, "node_modules/unique-filename": { diff --git a/package.json b/package.json index 9517b02..65478aa 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,10 @@ "build": "npx tsc", "build:mini": "npx tsc && bash ./src/misc/minifyDist.sh --build-only", "mini": "bash ./src/misc/minifyDist.sh", - "docker": "sudo docker compose up", - "docker:build": "sudo docker build . -t \"dockstatapi:local\" && sudo docker compose up" + "docker": "sudo docker compose up -d", + "docker:full": "docker compose up -d && [ -z \"$TMUX\" ] && tmux new-session -d -s docker 'docker compose logs -f master' \\; split-window -v 'docker compose logs -f slave' \\; attach-session || echo 'Already inside a tmux session. Exiting.'; docker compose down", + "docker:build": "docker build . -t \"dockstatapi:local\" && docker compose up -d", + "docker:build:full": "npm run docker:build && [ -z \"$TMUX\" ] && tmux new-session -d -s docker 'docker compose up -d && docker compose logs -f master' \\; split-window -v 'docker compose logs -f slave' \\; attach-session || echo 'Already inside a tmux session. Exiting.'; docker compose down" }, "keywords": [], "author": "Its4Nik", diff --git a/src/config/loggerConfig.ts b/src/config/loggerConfig.ts index 7d34f03..5d1a33e 100644 --- a/src/config/loggerConfig.ts +++ b/src/config/loggerConfig.ts @@ -8,6 +8,16 @@ const green = "\x1b[32m"; const yellow = "\x1b[33m"; const blue = "\x1b[34m"; +const ignoreExitListenerLogs = format((info) => { + if ( + typeof info.message === "string" && + info.message.includes("Exit listeners detected") + ) { + return false; // Silences annoying logs + } + return info; +}); + function colorLog(level: string, levelName: string) { switch (level) { case "info": @@ -26,6 +36,7 @@ function colorLog(level: string, levelName: string) { const logger = createLogger({ level: "debug", format: format.combine( + ignoreExitListenerLogs(), format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), format.printf((info) => { const level = info.level.toUpperCase().padEnd(5, " "); diff --git a/src/data/frontendConfiguration.json b/src/data/frontendConfiguration.json new file mode 100644 index 0000000..4697f96 --- /dev/null +++ b/src/data/frontendConfiguration.json @@ -0,0 +1,8 @@ +[ + { + "name": "test", + "tags": [ + "123" + ] + } +] \ No newline at end of file diff --git a/src/data/usePassword.txt b/src/data/usePassword.txt index c508d53..02e4a84 100644 --- a/src/data/usePassword.txt +++ b/src/data/usePassword.txt @@ -1 +1 @@ -false +false \ No newline at end of file diff --git a/src/init.ts b/src/init.ts index 6d3854a..feaa00d 100644 --- a/src/init.ts +++ b/src/init.ts @@ -47,29 +47,6 @@ const initializeApp = (app: express.Application): void => { process.on("exit", (code: number) => { logger.warn(`Server exiting (Code: ${code})`); - console.log(` - \u001b[1;31mThank you for using\u001b[0m - - \u001b[1;34m###### ###### #### ### ### #### ######### ###### #########\u001b[0m - \u001b[1;34m### ### ### ### ### ### ### ### ### ### ### ###\u001b[0m - \u001b[1;34m### ### ### ### ### ###### #### ### ### ### ###\u001b[0m - \u001b[1;34m### ### ### ### ### ### ### #### ### ############ ###\u001b[0m - \u001b[1;34m### ### ### ### ### ### ### #### ### ### ### ###\u001b[0m - \u001b[1;34m###### ###### #### ### ### #### ### ### ### ### \u001b[0m(\u001b[1;33mAPI - v2.0.0\u001b[0m) - - \u001b[1;36mUseful links before you go:\u001b[0m - - - Documentation: \u001b[1;32mhttps://outline.itsnik.de/s/dockstat\u001b[0m - - GitHub (Frontend): \u001b[1;32mhttps://github.com/its4nik/dockstat\u001b[0m - - GitHub (Backend): \u001b[1;32mhttps://github.com/its4nik/dockstatapi\u001b[0m - - API Documentation: \u001b[1;32mhttp://localhost:7000/api-docs\u001b[0m - - \u001b[1;35mSummary:\u001b[0m - - DockStat and DockStatAPI are 2 fully OpenSource projects, DockStatAPI is a simple but extensible API which allows queries via a REST endpoint. - - \u001b[1;31mGoodbye! We hope to see you again soon.\u001b[0m - `); }); }; diff --git a/src/server.ts b/src/server.ts index 4853204..6b68029 100644 --- a/src/server.ts +++ b/src/server.ts @@ -7,11 +7,13 @@ import writeUserConf from "./config/hostsystem"; const app = express(); const PORT: number = 9876; +logger.info("Server starting up..."); +logger.info(`Server is running on http://localhost:${PORT}`); +logger.info(`Swagger docs available at http://localhost:${PORT}/api-docs\n`); + writeUserConf(); initializeApp(app); app.listen(PORT, () => { - logger.info(`Server is running on http://localhost:${PORT}`); - logger.info(`Swagger docs available at http://localhost:${PORT}/api-docs`); startMasterNode(); }); From 45e3fc1f2f167a38ec011a198f27d810630e3016 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 25 Dec 2024 20:16:46 +0100 Subject: [PATCH 037/324] Chore: adjust process env and patch to 2.0.1 --- Dockerfile | 2 +- nodemon.json | 11 +++++- package.json | 17 +++----- src/config/hostsystem.ts | 5 ++- src/config/variables.ts | 24 ++++++++++++ src/controllers/fetchData.ts | 26 ++++++------ src/controllers/highAvailability.ts | 26 +++++++----- src/controllers/notificationController.ts | 48 +++++++++++------------ src/controllers/proxy.ts | 3 +- src/data/template.json | 3 ++ src/init.ts | 22 +++++------ src/misc/createEnvDev.sh | 32 +++++++++++++++ src/misc/createEnvFile.sh | 13 +++--- src/misc/entrypoint.sh | 5 +-- src/routes/getter/routes.ts | 35 +++++++---------- src/utils/notifications/_notify.ts | 33 ---------------- src/utils/notifications/discord.ts | 19 ++++----- src/utils/notifications/email.ts | 14 +++++-- src/utils/notifications/pushbullet.ts | 4 +- src/utils/notifications/pushover.ts | 25 ++++++------ src/utils/notifications/slack.ts | 19 ++++----- src/utils/notifications/telegram.ts | 23 +++++------ src/utils/notifications/whatsapp.ts | 21 +++++----- tsconfig.json | 12 ++---- 24 files changed, 233 insertions(+), 209 deletions(-) create mode 100644 src/config/variables.ts create mode 100644 src/data/template.json create mode 100755 src/misc/createEnvDev.sh diff --git a/Dockerfile b/Dockerfile index 78ee53b..53f3b72 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM node:alpine AS builder LABEL maintainer="https://github.com/its4nik" -LABEL version="2.0.0" +LABEL version="2.0.1" LABEL description="API for DockStat" LABEL license="BSD-3-Clause license" LABEL repository="https://github.com/its4nik/dockstatapi" diff --git a/nodemon.json b/nodemon.json index 30602eb..9d946e9 100644 --- a/nodemon.json +++ b/nodemon.json @@ -1,6 +1,13 @@ { - "ignore": ["src/logs", "**/fixtures/**", ".gitignore", "**/*.json"], + "ignore": [ + "**/data/**", + "src/logs", + "**/fixtures/**", + ".gitignore", + "**/*.json" + ], "execMap": { "ts": "tsx" - } + }, + "delay": 2500 } diff --git a/package.json b/package.json index 65478aa..d600a79 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,14 @@ { "name": "dockstatapi", - "version": "2.0.0", + "version": "2.0.1", "description": "API for docker hosts using dockerode", "main": "src/server.ts", "scripts": { - "start": "tsx src/server.ts", + "local-env-file": "bash ./src/misc/createEnvDev.sh", + "start": "npm run local-env-file && tsx src/server.ts", "start:build": "npx tsc && node dist/server.js", - "dev": "nodemon", - "dev:trace": "nodemon --trace-uncaught --trace-warnings", + "dev": "npm run local-env-file && nodemon", + "dev:trace": "npm run local-env-file && nodemon --trace-uncaught --trace-warnings", "dep": "bash ./src/utils/createDependencyGraph.sh", "dep:remove": "bash ./src/utils/removeUnusedDeps.sh && bash ./src/utils/createDependencyGraph.sh", "build": "npx tsc", @@ -57,14 +58,6 @@ "tsx": "^4.19.2", "uglify-js": "^3.19.3" }, - "nodemonConfig": { - "ignore": [ - "**/data/**", - "**/*.json", - ".gitignore" - ], - "delay": 2500 - }, "engines": { "npm": ">=10.8.2" }, diff --git a/src/config/hostsystem.ts b/src/config/hostsystem.ts index 520d3ef..8b4227f 100644 --- a/src/config/hostsystem.ts +++ b/src/config/hostsystem.ts @@ -1,10 +1,11 @@ +import { RUNNING_IN_DOCKER, VERSION } from "./variables"; import fs from "fs"; import logger from "../utils/logger"; import os from "os"; const userConf = "./src/data/user.conf"; -const inDocker: boolean = !!process.env.RUNNING_IN_DOCKER; -const version: string = process.env.VERSION || "unknown"; +const inDocker: boolean = RUNNING_IN_DOCKER == "true"; +const version: string = VERSION || "unknown"; function writeUserConf() { let previousConfig = null; diff --git a/src/config/variables.ts b/src/config/variables.ts new file mode 100644 index 0000000..26a522b --- /dev/null +++ b/src/config/variables.ts @@ -0,0 +1,24 @@ +import vars from "../data/variables.json"; + +export const { + VERSION, + RUNNING_IN_DOCKER, + TRUSTED_PROXYS, + HA_MASTER, + HA_MASTER_IP, + HA_NODE, + HA_UNSAFE, + DISCORD_WEBHOOK_URL, + EMAIL_SENDER, + EMAIL_RECIPIENT, + EMAIL_PASSWORD, + EMAIL_SERVICE, + PUSHBULLET_ACCESS_TOKEN, + PUSHOVER_USER_KEY, + PUSHOVER_API_TOKEN, + SLACK_WEBHOOK_URL, + TELEGRAM_BOT_TOKEN, + TELEGRAM_CHAT_ID, + WHATSAPP_API_URL, + WHATSAPP_RECIPIENT, +} = vars; diff --git a/src/controllers/fetchData.ts b/src/controllers/fetchData.ts index be9fdc7..238e826 100644 --- a/src/controllers/fetchData.ts +++ b/src/controllers/fetchData.ts @@ -22,21 +22,17 @@ const fetchData = async (): Promise => { const allContainerData: AllContainerData = (await fetchAllContainers()) || {}; - if (process.env.OFFLINE === "true") { - logger.info("No new data inserted --- OFFLINE MODE"); - } else { - db.run( - `INSERT INTO data (info) VALUES (?)`, - [JSON.stringify(allContainerData)], - function (error) { - if (error) { - logger.error("Error inserting data:", error); - return; - } - logger.info(`Data inserted with ID: ${this.lastID}`); - }, - ); - } + db.run( + `INSERT INTO data (info) VALUES (?)`, + [JSON.stringify(allContainerData)], + function (error) { + if (error) { + logger.error("Error inserting data:", error); + return; + } + logger.info(`Data inserted with ID: ${this.lastID}`); + }, + ); const containerStatus: AllContainerData = {}; diff --git a/src/controllers/highAvailability.ts b/src/controllers/highAvailability.ts index e855757..f02bde9 100644 --- a/src/controllers/highAvailability.ts +++ b/src/controllers/highAvailability.ts @@ -3,6 +3,12 @@ import fs from "fs"; import chokidar from "chokidar"; import path from "path"; import { promisify } from "util"; +import { + HA_UNSAFE, + HA_MASTER, + HA_MASTER_IP, + HA_NODE, +} from "../config/variables"; const sleep = promisify(setTimeout); @@ -28,7 +34,7 @@ interface NodeCache { const haMasterPath: string = "./src/data/highAvailability.json"; const haNodePath: string = "./src/data/haNode.json"; const nodeCachePath: string = "./src/data/nodeCache.json"; -const useUnsafeConnection = process.env.HA_UNSAFE || "false"; +const useUnsafeConnection: boolean = HA_UNSAFE == "false"; const lockFilePath: string = "./src/data/ha.lock"; const configFiles: string[] = [ @@ -39,6 +45,7 @@ const configFiles: string[] = [ "./src/data/nodeCache.json", "./src/data/usePassword.txt", "./src/data/password.json", + "./src/data/variables.json", ]; async function acquireLock(): Promise { @@ -119,7 +126,7 @@ async function prepareFilesForSync(): Promise> { async function checkApiReachable(node: string): Promise { let nodeUrl = - useUnsafeConnection === "true" + useUnsafeConnection === true ? `http://${node}/api/status` : `https://${node}/api/status`; @@ -163,7 +170,7 @@ async function synchronizeFilesWithNodes(): Promise { } let nodeUrl = - useUnsafeConnection == "true" + useUnsafeConnection == true ? `http://${node}/ha/sync` : `https://${node}/ha/sync`; @@ -201,8 +208,9 @@ function monitorConfigFiles(): void { } async function startMasterNode() { - if (process.env.HA_MASTER == "true") { - if (!process.env.HA_MASTER_IP) { + let isMaster: boolean = HA_MASTER == "false"; + if (isMaster) { + if (!HA_MASTER_IP) { logger.error( "Master's IP is not set, please set the HA_MASTER_IP variable (example: 10.0.0.4:9876)", ); @@ -213,13 +221,11 @@ async function startMasterNode() { const haConfig: HighAvailabilityConfig = { active: true, master: true, - nodes: process.env.HA_NODE - ? process.env.HA_NODE.split(",").map((node) => node.trim()) - : [], + nodes: HA_NODE ? HA_NODE.split(",").map((node) => node.trim()) : [], }; - const nodeCache: NodeCache = process.env.HA_NODE - ? process.env.HA_NODE.split(",").reduce((cache, node, index) => { + const nodeCache: NodeCache = HA_NODE + ? HA_NODE.split(",").reduce((cache, node, index) => { const [ip, id] = node.trim().split(":"); if (ip && id) { cache[`node${index + 1}`] = { ip, id: parseInt(id, 10) }; diff --git a/src/controllers/notificationController.ts b/src/controllers/notificationController.ts index e34eecd..ad0b1bc 100644 --- a/src/controllers/notificationController.ts +++ b/src/controllers/notificationController.ts @@ -1,20 +1,29 @@ import notify from "../utils/notifications/_notify"; import logger from "../utils/logger"; +import { + DISCORD_WEBHOOK_URL, + EMAIL_SENDER, + EMAIL_RECIPIENT, + EMAIL_PASSWORD, + EMAIL_SERVICE, + PUSHBULLET_ACCESS_TOKEN, + PUSHOVER_USER_KEY, + PUSHOVER_API_TOKEN, + SLACK_WEBHOOK_URL, + TELEGRAM_BOT_TOKEN, + TELEGRAM_CHAT_ID, + WHATSAPP_API_URL, + WHATSAPP_RECIPIENT, +} from "../config/variables"; const notificationTypes = { - discord: !!process.env.DISCORD_WEBHOOK_URL, - email: !!( - process.env.EMAIL_SENDER && - process.env.EMAIL_RECIPIENT && - process.env.EMAIL_PASSWORD - ), - pushbullet: !!process.env.PUSHBULLET_ACCESS_TOKEN, - pushover: !!(process.env.PUSHOVER_API_TOKEN && process.env.PUSHOVER_USER_KEY), - slack: !!process.env.SLACK_WEBHOOK_UR, - telegram: !!(process.env.TELEGRAM_BOT_TOKEN && process.env.TELEGRAM_CHAT_ID), - whatsapp: !!(process.env.WHATSAPP_API_URL && process.env.WHATSAPP_RECIPIENT), - custom: !!process.env.CUSTOM_NOTIFICATION, - customList: process.env.CUSTOM_NOTIFICATION, + discord: !!DISCORD_WEBHOOK_URL, + email: !!(EMAIL_SENDER && EMAIL_RECIPIENT && EMAIL_PASSWORD && EMAIL_SERVICE), + pushbullet: !!PUSHBULLET_ACCESS_TOKEN, + pushover: !!(PUSHOVER_API_TOKEN && PUSHOVER_USER_KEY), + slack: !!SLACK_WEBHOOK_URL, + telegram: !!(TELEGRAM_BOT_TOKEN && TELEGRAM_CHAT_ID), + whatsapp: !!(WHATSAPP_API_URL && WHATSAPP_RECIPIENT), }; async function sendNotification(containerId: string) { @@ -46,17 +55,4 @@ async function sendNotification(containerId: string) { logger.debug(`Sending notification via Pushbullet (${containerId})`); notify("whatsapp", containerId); } - if (notificationTypes.custom) { - const elements: undefined | string[] = notificationTypes.customList - ? notificationTypes.customList.split(",") - : undefined; - if (elements) { - elements.forEach((element) => { - logger.debug(`Sending custom notification ${element} (${containerId})`); - notify(`custom/${element}`, containerId); - }); - } else { - logger.error("Error getting custom notifications"); - } - } } diff --git a/src/controllers/proxy.ts b/src/controllers/proxy.ts index 681adef..601f155 100644 --- a/src/controllers/proxy.ts +++ b/src/controllers/proxy.ts @@ -1,8 +1,9 @@ import { Application } from "express"; import logger from "../utils/logger"; +import { TRUSTED_PROXYS } from "../config/variables"; export default function trustedProxies(app: Application) { - const trusted: string = process.env.TRUSTED_PROXYS || ""; + const trusted: string = TRUSTED_PROXYS; if (!trusted) { logger.warn( diff --git a/src/data/template.json b/src/data/template.json new file mode 100644 index 0000000..75e12f2 --- /dev/null +++ b/src/data/template.json @@ -0,0 +1,3 @@ +{ + "text": "{{name}} is {{state}} on {{hostName}}" +} diff --git a/src/init.ts b/src/init.ts index feaa00d..eb3612b 100644 --- a/src/init.ts +++ b/src/init.ts @@ -16,6 +16,8 @@ import cors from "cors"; import { blockWhileLocked } from "./middleware/checkLock"; import logger from "./utils/logger"; +const LAB = [limiter, authMiddleware, blockWhileLocked]; + const initializeApp = (app: express.Application): void => { app.use(cors()); app.use(express.json()); @@ -24,21 +26,15 @@ const initializeApp = (app: express.Application): void => { ); swaggerDocs(app as any); - trustedProxies(app); // Configures proxies using CSV string + trustedProxies(app); scheduleFetch(); - app.use("/api", limiter, authMiddleware, blockWhileLocked, api); - app.use("/conf", limiter, authMiddleware, blockWhileLocked, conf); - app.use("/auth", limiter, authMiddleware, blockWhileLocked, auth); - app.use("/data", limiter, authMiddleware, blockWhileLocked, data); - app.use("/frontend", limiter, authMiddleware, blockWhileLocked, frontend); - app.use( - "/notification-service", - limiter, - authMiddleware, - blockWhileLocked, - notificationService, - ); + app.use("/api", LAB, api); + app.use("/conf", LAB, conf); + app.use("/auth", LAB, auth); + app.use("/data", LAB, data); + app.use("/frontend", LAB, frontend); + app.use("/notification-service", LAB, notificationService); app.use("/ha", limiter, authMiddleware, ha); app.get("/", (req: Request, res: Response) => { diff --git a/src/misc/createEnvDev.sh b/src/misc/createEnvDev.sh new file mode 100755 index 0000000..dde36f6 --- /dev/null +++ b/src/misc/createEnvDev.sh @@ -0,0 +1,32 @@ +VERSION="$(cat ./package.json | grep version | cut -d '"' -f 4)" + +if grep -q '/docker' /proc/1/cgroup 2>/dev/null || [ -f /.dockerenv ]; then + RUNNING_IN_DOCKER="true" +else + RUNNING_IN_DOCKER="false" +fi + +echo -n "\ +{ + \"VERSION\": \"${VERSION}\", + \"RUNNING_IN_DOCKER\": \"${RUNNING_IN_DOCKER}\", + \"TRUSTED_PROXYS\": \"${TRUSTED_PROXYS}\", + \"HA_MASTER\": \"${HA_MASTER}\", + \"HA_MASTER_IP\": \"${HA_MASTER_IP}\", + \"HA_NODE\": \"${HA_NODE}\", + \"HA_UNSAFE\": \"${HA_UNSAFE}\", + \"DISCORD_WEBHOOK_URL\": \"${DISCORD_WEBHOOK_URL}\", + \"EMAIL_SENDER\": \"${EMAIL_SENDER}\", + \"EMAIL_RECIPIENT\": \"${EMAIL_RECIPIENT}\", + \"EMAIL_PASSWORD\": \"${EMAIL_PASSWORD}\", + \"EMAIL_SERVICE\": \"${EMAIL_SERVICE}\", + \"PUSHBULLET_ACCESS_TOKEN\": \"${PUSHBULLET_ACCESS_TOKEN}\", + \"PUSHOVER_USER_KEY\": \"${PUSHOVER_USER_KEY}\", + \"PUSHOVER_API_TOKEN\": \"${PUSHOVER_API_TOKEN}\", + \"SLACK_WEBHOOK_URL\": \"${SLACK_WEBHOOK_URL}\", + \"TELEGRAM_BOT_TOKEN\": \"${TELEGRAM_BOT_TOKEN}\", + \"TELEGRAM_CHAT_ID\": \"${TELEGRAM_CHAT_ID}\", + \"WHATSAPP_API_URL\": \"${WHATSAPP_API_URL}\", + \"WHATSAPP_RECIPIENT\": \"${WHATSAPP_RECIPIENT}\" +} \ +" > ./src/data/variables.json diff --git a/src/misc/createEnvFile.sh b/src/misc/createEnvFile.sh index cbd8244..d47eaa9 100644 --- a/src/misc/createEnvFile.sh +++ b/src/misc/createEnvFile.sh @@ -1,7 +1,7 @@ #!/bin/bash # Version -VERSION="$1" +VERSION="$(cat ./package.json | grep version | cut -d '"' -f 4)" # Docker if grep -q '/docker' /proc/1/cgroup 2>/dev/null || [ -f /.dockerenv ]; then @@ -9,9 +9,11 @@ if grep -q '/docker' /proc/1/cgroup 2>/dev/null || [ -f /.dockerenv ]; then else RUNNING_IN_DOCKER="false" fi -echo " +echo -n "\ { + \"VERSION\": \"${VERSION}\", \"RUNNING_IN_DOCKER\": \"${RUNNING_IN_DOCKER}\", + \"TRUSTED_PROXYS\": \"${TRUSTED_PROXYS}\", \"HA_MASTER\": \"${HA_MASTER}\", \"HA_MASTER_IP\": \"${HA_MASTER_IP}\", \"HA_NODE\": \"${HA_NODE}\", @@ -28,7 +30,6 @@ echo " \"TELEGRAM_BOT_TOKEN\": \"${TELEGRAM_BOT_TOKEN}\", \"TELEGRAM_CHAT_ID\": \"${TELEGRAM_CHAT_ID}\", \"WHATSAPP_API_URL\": \"${WHATSAPP_API_URL}\", - \"WHATSAPP_RECIPIENT\": \"${WHATSAPP_RECIPIENT}\", - \"CUSTOM_NOTIFICATION\": \"${CUSTOM_NOTIFICATION}\" -} -" > /api/src/data/variables.conf + \"WHATSAPP_RECIPIENT\": \"${WHATSAPP_RECIPIENT}\" +} \ +" > /api/src/data/variables.json diff --git a/src/misc/entrypoint.sh b/src/misc/entrypoint.sh index ff5cc61..83eaf46 100755 --- a/src/misc/entrypoint.sh +++ b/src/misc/entrypoint.sh @@ -1,6 +1,6 @@ #!/bin/bash -VERSION="2.0.0" +VERSION="$(cat ./package.json | grep version | cut -d '"' -f 4)" echo -e " \033[1;32mWelcome to\033[0m @@ -17,7 +17,6 @@ echo -e " - Documentation: \033[1;32mhttps://outline.itsnik.de/s/dockstat\033[0m - GitHub (Frontend): \033[1;32mhttps://github.com/its4nik/dockstat\033[0m - GitHub (Backend): \033[1;32mhttps://github.com/its4nik/dockstatapi\033[0m -- API Documentation: \033[1;32mhttp://localhost:7000/api-docs\033[0m \033[1;35mSummary:\033[0m @@ -25,6 +24,6 @@ DockStat and DockStatAPI are 2 fully OpenSource projects, DockStatAPI is a simpl " -bash "./createEnvFile.sh" "$VERSION" +bash "./createEnvFile.sh" exec node src/server.js diff --git a/src/routes/getter/routes.ts b/src/routes/getter/routes.ts index c559e63..b6c89c1 100644 --- a/src/routes/getter/routes.ts +++ b/src/routes/getter/routes.ts @@ -134,28 +134,23 @@ router.get("/system", (req: Request, res: Response) => { * description: Error message detailing the issue encountered. */ router.get("/host/:hostName/stats", async (req: Request, res: Response) => { - const {hostName} = req.params; + const { hostName } = req.params; logger.info(`Fetching stats for host: ${hostName}`); - if (process.env.OFFLINE === "true") { - logger.info("Fetching offline Host Stats"); - res.status(200).json(readOfflineLog); - } else { - try { - const docker = getDockerClient(hostName); - const info = await docker.info(); - const version = await docker.version(); - const relevantData = extractRelevantData({ hostName, info, version }); + try { + const docker = getDockerClient(hostName); + const info = await docker.info(); + const version = await docker.version(); + const relevantData = extractRelevantData({ hostName, info, version }); - writeOfflineLog(JSON.stringify(relevantData)); - res.status(200).json(relevantData); - } catch (error: any) { - logger.error( - `Error fetching stats for host: ${hostName} - ${error.message || "Unknown error"}`, - ); - res.status(500).json({ - error: `Error fetching host stats: ${error.message || "Unknown error"}`, - }); - } + writeOfflineLog(JSON.stringify(relevantData)); + res.status(200).json(relevantData); + } catch (error: any) { + logger.error( + `Error fetching stats for host: ${hostName} - ${error.message || "Unknown error"}`, + ); + res.status(500).json({ + error: `Error fetching host stats: ${error.message || "Unknown error"}`, + }); } }); diff --git a/src/utils/notifications/_notify.ts b/src/utils/notifications/_notify.ts index 018b3dc..139a006 100644 --- a/src/utils/notifications/_notify.ts +++ b/src/utils/notifications/_notify.ts @@ -6,28 +6,6 @@ import { emailNotification } from "./email"; import { whatsappNotification } from "./whatsapp"; import { pushbulletNotification } from "./pushbullet"; import { pushoverNotification } from "./pushover"; -import path from "path"; - -async function loadCustomNotification(scriptPath: string, containerId: string) { - try { - const absolutePath = path.resolve(__dirname, "./custom", scriptPath); - const customModule = await import(absolutePath); - - if (typeof customModule.default !== "function") { - const errorMsg = `The custom notification script at ${scriptPath} does not export a default function.`; - logger.error(errorMsg); - throw new Error(errorMsg); - } - - logger.debug(`Executing custom notification script: ${scriptPath}`); - await customModule.default(containerId); - } catch (error: any) { - logger.error( - `Failed to execute custom notification script (${scriptPath}): ${error.message}`, - ); - throw error; - } -} async function notify(type: string, containerId: string) { if (!containerId) { @@ -35,17 +13,6 @@ async function notify(type: string, containerId: string) { throw new Error("Container ID is required."); } - if (type.startsWith("custom/")) { - const scriptName = type.split("/")[1]; - if (!scriptName) { - const errorMsg = "Custom notification script name is invalid."; - logger.error(errorMsg); - throw new Error(errorMsg); - } - await loadCustomNotification(`${scriptName}.js`, containerId); - return; - } - switch (type) { case "telegram": logger.debug("Sending Telegram notification..."); diff --git a/src/utils/notifications/discord.ts b/src/utils/notifications/discord.ts index 24aaf90..d9be3a0 100644 --- a/src/utils/notifications/discord.ts +++ b/src/utils/notifications/discord.ts @@ -1,8 +1,9 @@ -import * as https from 'https'; +import * as https from "https"; import logger from "../logger"; import { renderTemplate } from "./_template"; +import { DISCORD_WEBHOOK_URL } from "../../config/variables"; -const discord_webhook_url: string | undefined = process.env.DISCORD_WEBHOOK_URL; +const discord_webhook_url: string = DISCORD_WEBHOOK_URL; export async function discordNotification(containerId: string): Promise { const discord_message: string | null = renderTemplate(containerId); @@ -25,28 +26,28 @@ export async function discordNotification(containerId: string): Promise { const options = { hostname: url.hostname, path: url.pathname, - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', - 'Content-Length': Buffer.byteLength(postData), + "Content-Type": "application/json", + "Content-Length": Buffer.byteLength(postData), }, }; const req = https.request(options, (res) => { - let data = ''; + let data = ""; - res.on('data', (chunk) => { + res.on("data", (chunk) => { data += chunk; }); - res.on('end', () => { + res.on("end", () => { if (res.statusCode !== 200) { logger.error(`Discord API error: ${data}`); } }); }); - req.on('error', (error) => { + req.on("error", (error) => { logger.error("Error sending Discord message:", error); }); diff --git a/src/utils/notifications/email.ts b/src/utils/notifications/email.ts index fbefbab..57c94ef 100644 --- a/src/utils/notifications/email.ts +++ b/src/utils/notifications/email.ts @@ -1,11 +1,17 @@ import { SendMailOptions, createTransport } from "nodemailer"; import logger from "../logger"; import { renderTemplate } from "./_template"; +import { + EMAIL_SENDER, + EMAIL_SERVICE, + EMAIL_PASSWORD, + EMAIL_RECIPIENT, +} from "../../config/variables"; -const email_sender: string | undefined = process.env.EMAIL_SENDER; -const email_recipient: string | undefined = process.env.EMAIL_RECIPIENT; -const email_password: string | undefined = process.env.EMAIL_PASSWORD; -const email_service: string | undefined = process.env.EMAIL_SERVICE; +const email_sender: string = EMAIL_SENDER; +const email_recipient: string = EMAIL_RECIPIENT; +const email_password: string = EMAIL_PASSWORD; +const email_service: string = EMAIL_SERVICE; export async function emailNotification(containerId: string) { // Validate email configuration parameters diff --git a/src/utils/notifications/pushbullet.ts b/src/utils/notifications/pushbullet.ts index f008e68..811427a 100644 --- a/src/utils/notifications/pushbullet.ts +++ b/src/utils/notifications/pushbullet.ts @@ -1,9 +1,9 @@ import * as https from "https"; import logger from "../logger"; import { renderTemplate } from "./_template"; +import { PUSHBULLET_ACCESS_TOKEN } from "../../config/variables"; -const pushbullet_access_token: string | undefined = - process.env.PUSHBULLET_ACCESS_TOKEN; +const pushbullet_access_token: string = PUSHBULLET_ACCESS_TOKEN; export async function pushbulletNotification( containerId: string, diff --git a/src/utils/notifications/pushover.ts b/src/utils/notifications/pushover.ts index 847c329..aac71b3 100644 --- a/src/utils/notifications/pushover.ts +++ b/src/utils/notifications/pushover.ts @@ -1,9 +1,10 @@ -import * as https from 'https'; +import * as https from "https"; import logger from "../logger"; import { renderTemplate } from "./_template"; +import { PUSHOVER_USER_KEY, PUSHOVER_API_TOKEN } from "../../config/variables"; -const pushover_user_key: string | undefined = process.env.PUSHOVER_USER_KEY; -const pushover_api_token: string | undefined = process.env.PUSHOVER_API_TOKEN; +const pushover_user_key: string = PUSHOVER_USER_KEY; +const pushover_api_token: string = PUSHOVER_API_TOKEN; export async function pushoverNotification(containerId: string): Promise { const pushover_message: string | null = renderTemplate(containerId); @@ -24,30 +25,30 @@ export async function pushoverNotification(containerId: string): Promise { }).toString(); const options = { - hostname: 'api.pushover.net', - path: '/1/messages.json', - method: 'POST', + hostname: "api.pushover.net", + path: "/1/messages.json", + method: "POST", headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Content-Length': Buffer.byteLength(postData), + "Content-Type": "application/x-www-form-urlencoded", + "Content-Length": Buffer.byteLength(postData), }, }; const req = https.request(options, (res) => { - let data = ''; + let data = ""; - res.on('data', (chunk) => { + res.on("data", (chunk) => { data += chunk; }); - res.on('end', () => { + res.on("end", () => { if (res.statusCode !== 200) { logger.error(`Pushover API error: ${data}`); } }); }); - req.on('error', (error) => { + req.on("error", (error) => { logger.error("Error sending Pushover message:", error); }); diff --git a/src/utils/notifications/slack.ts b/src/utils/notifications/slack.ts index b0a8e0b..e1e7216 100644 --- a/src/utils/notifications/slack.ts +++ b/src/utils/notifications/slack.ts @@ -1,8 +1,9 @@ -import * as https from 'https'; +import * as https from "https"; import logger from "../logger"; import { renderTemplate } from "./_template"; +import { SLACK_WEBHOOK_URL } from "../../config/variables"; -const slack_webhook_url: string | undefined = process.env.SLACK_WEBHOOK_URL; +const slack_webhook_url: string = SLACK_WEBHOOK_URL; export async function slackNotification(containerId: string): Promise { const slack_message: string | null = renderTemplate(containerId); @@ -25,28 +26,28 @@ export async function slackNotification(containerId: string): Promise { const options = { hostname: url.hostname, path: url.pathname, - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', - 'Content-Length': Buffer.byteLength(postData), + "Content-Type": "application/json", + "Content-Length": Buffer.byteLength(postData), }, }; const req = https.request(options, (res) => { - let data = ''; + let data = ""; - res.on('data', (chunk) => { + res.on("data", (chunk) => { data += chunk; }); - res.on('end', () => { + res.on("end", () => { if (res.statusCode !== 200) { logger.error(`Slack API error: ${data}`); } }); }); - req.on('error', (error) => { + req.on("error", (error) => { logger.error("Error sending Slack message:", error); }); diff --git a/src/utils/notifications/telegram.ts b/src/utils/notifications/telegram.ts index 174a12e..440e091 100644 --- a/src/utils/notifications/telegram.ts +++ b/src/utils/notifications/telegram.ts @@ -1,9 +1,10 @@ -import * as https from 'https'; +import * as https from "https"; import logger from "../logger"; import { renderTemplate } from "./_template"; +import { TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID } from "../../config/variables"; -const telegram_bot_token: string | undefined = process.env.TELEGRAM_BOT_TOKEN; -const telegram_chat_id: string | undefined = process.env.TELEGRAM_CHAT_ID; +const telegram_bot_token: string = TELEGRAM_BOT_TOKEN; +const telegram_chat_id: string = TELEGRAM_CHAT_ID; export async function telegramNotification(containerId: string): Promise { const telegram_message: string | null = renderTemplate(containerId); @@ -23,30 +24,30 @@ export async function telegramNotification(containerId: string): Promise { }); const options = { - hostname: 'api.telegram.org', + hostname: "api.telegram.org", path: `/bot${telegram_bot_token}/sendMessage`, - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', - 'Content-Length': Buffer.byteLength(postData), + "Content-Type": "application/json", + "Content-Length": Buffer.byteLength(postData), }, }; const req = https.request(options, (res) => { - let data = ''; + let data = ""; - res.on('data', (chunk) => { + res.on("data", (chunk) => { data += chunk; }); - res.on('end', () => { + res.on("end", () => { if (res.statusCode !== 200) { logger.error(`Telegram API error: ${data}`); } }); }); - req.on('error', (error) => { + req.on("error", (error) => { logger.error("Error sending message:", error); }); diff --git a/src/utils/notifications/whatsapp.ts b/src/utils/notifications/whatsapp.ts index 178f6d5..1eb7575 100644 --- a/src/utils/notifications/whatsapp.ts +++ b/src/utils/notifications/whatsapp.ts @@ -1,9 +1,10 @@ -import * as https from 'https'; +import * as https from "https"; import logger from "../logger"; import { renderTemplate } from "./_template"; +import { WHATSAPP_API_URL, WHATSAPP_RECIPIENT } from "../../config/variables"; -const whatsapp_api_url: string | undefined = process.env.WHATSAPP_API_URL; -const whatsapp_recipient: string | undefined = process.env.WHATSAPP_RECIPIENT; +const whatsapp_api_url: string = WHATSAPP_API_URL; +const whatsapp_recipient: string = WHATSAPP_RECIPIENT; export async function whatsappNotification(containerId: string): Promise { const whatsapp_message: string | null = renderTemplate(containerId); @@ -27,28 +28,28 @@ export async function whatsappNotification(containerId: string): Promise { const options = { hostname: url.hostname, path: url.pathname, - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', - 'Content-Length': Buffer.byteLength(postData), + "Content-Type": "application/json", + "Content-Length": Buffer.byteLength(postData), }, }; const req = https.request(options, (res) => { - let data = ''; + let data = ""; - res.on('data', (chunk) => { + res.on("data", (chunk) => { data += chunk; }); - res.on('end', () => { + res.on("end", () => { if (res.statusCode !== 200) { logger.error(`WhatsApp API error: ${data}`); } }); }); - req.on('error', (error) => { + req.on("error", (error) => { logger.error("Error sending WhatsApp message:", error); }); diff --git a/tsconfig.json b/tsconfig.json index 8fc3c32..4af6b1d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "resolveJsonModule": true, "target": "ES2020", "outDir": "dist/src", "module": "CommonJS", @@ -11,11 +12,6 @@ }, "$schema": "https://json.schemastore.org/tsconfig", "display": "Recommended", - "include": [ - "src/**/*" - ], - "exclude": [ - "node_modules", - "**/*.spec.ts" - ] -} \ No newline at end of file + "include": ["src/**/*"], + "exclude": ["node_modules", "**/*.spec.ts"] +} From 39445445b7a7c14c59905bb6a3402015ba7fbc20 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 25 Dec 2024 20:17:23 +0100 Subject: [PATCH 038/324] Fix: update .gitignore --- .gitignore | 1 + TODO.md | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index c7f5c64..ee4e7af 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ src/data/user.conf src/data/password.json src/data/ha.lock src/data/frontendConfiguration.json +src/data/variables.json docker .test* diff --git a/TODO.md b/TODO.md index d2659e2..c4687d7 100644 --- a/TODO.md +++ b/TODO.md @@ -1,4 +1,4 @@ -- [ ] Better Offline mode using "faker" library or self written (probably self written) +- [X] ~Better Offline mode using "faker" library or self written (probably self written)~ Not needed since there is a docker-compsoe file for local testing integrated inside the repo - [X] HA compatibility - [X] !!! Needs testing !!! Add automatic notifications when container state changes, according to selected level for notification service - [ ] Image update and update notifications @@ -10,3 +10,4 @@ - [ ] Websockets - [X] Better /api/status endpoint with connection status of each host - [X] Update notification service +- [X] Adjust process.env variables since they don't really work as expected (See [commit](https://github.com/Its4Nik/dockstatapi/pull/21/commits/a03b58c7a17e269f46216df5492e18d008774961)) From b6338e9270b136eb21f6b652af39e1cc73f6885e Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 25 Dec 2024 23:37:29 +0100 Subject: [PATCH 039/324] Chore: Creating default files on container startup Fix: Fixing env variables Docker: Added dev Dockerfile without Swagger docs --- Dockerfile-dev | 61 +++++++++++++++++++++++++++++ docker-compose.yaml | 21 ++++++++-- package.json | 4 +- src/config/db.ts | 10 ++--- src/config/hostsystem.ts | 3 +- src/config/initFiles.ts | 41 +++++++++++++++++++ src/config/loggerConfig.ts | 1 + src/controllers/highAvailability.ts | 18 ++++----- src/init.ts | 2 + 9 files changed, 138 insertions(+), 23 deletions(-) create mode 100644 Dockerfile-dev create mode 100644 src/config/initFiles.ts diff --git a/Dockerfile-dev b/Dockerfile-dev new file mode 100644 index 0000000..7ad56f0 --- /dev/null +++ b/Dockerfile-dev @@ -0,0 +1,61 @@ +# Stage 1: Build stage +FROM node:alpine AS builder + +LABEL maintainer="https://github.com/its4nik" +LABEL version="2.0.1" +LABEL description="API for DockStat" +LABEL license="BSD-3-Clause license" +LABEL repository="https://github.com/its4nik/dockstatapi" +LABEL documentation="https://github.com/its4nik/dockstatapi" +LABEL org.opencontainers.image.description="The DockSatAPI is a free and OpenSource backend for gathering container statistics across hosts" +LABEL org.opencontainers.image.licenses="BSD-3-Clause license" +LABEL org.opencontainers.image.source="https://github.com/its4nik/dockstatapi" + +WORKDIR /build +ENV NODE_NO_WARNINGS=1 + +RUN apk update && \ + apk upgrade && \ + apk add bash + + +COPY tsconfig.json environment.d.ts package*.json tsconfig.json yarn.lock ./ +RUN npm install + +COPY ./src ./src +RUN npm run build + +# Stage 2: main stage +FROM alpine AS main + +# Needed packages +RUN apk update && \ + apk upgrade && \ + apk add --update npm + +WORKDIR /build + +RUN mkdir -p /build/src/data + +COPY tsconfig.json environment.d.ts package*.json tsconfig.json yarn.lock ./ +RUN npm install --omit=dev + +COPY --from=builder /build/dist/* /build/src +COPY --from=builder /build/src/misc/entrypoint.sh /build/entrypoint.sh +COPY --from=builder /build/src/misc/createEnvFile.sh /build/createEnvFile.sh + +RUN node src/config/db.js + +# Stage 3: Production stage +FROM alpine AS production + +RUN apk add --update bash curl nodejs +HEALTHCHECK --interval=5m --timeout=3s \ + CMD curl -f http://localhost:9876/api/status || exit 1 + +WORKDIR /api + +COPY --from=main /build /api + +EXPOSE 9876 +ENTRYPOINT [ "bash", "./entrypoint.sh" ] diff --git a/docker-compose.yaml b/docker-compose.yaml index 3158657..06d1f45 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,17 +1,26 @@ +networks: + shared-network: + driver: bridge + services: master: container_name: master environment: - NODE_ENV=development - HA_MASTER=true - - HA_MASTER_IP=127.0.0.1:9876 - - HA_NODE=127.0.0.1:6789 + - HA_MASTER_IP=master:9876 + - HA_NODE=slave:9876 - HA_UNSAFE=true volumes: - ./docker/master:/api/src/data ports: - 9876:9876 image: dockstatapi:local + networks: + - shared-network + depends_on: + - slave + - test-socket-proxy slave: container_name: slave environment: @@ -19,12 +28,14 @@ services: volumes: - ./docker/slave:/api/src/data ports: - - 6789:6789 + - 6789:9876 image: dockstatapi:local + networks: + - shared-network test-socket-proxy: image: lscr.io/linuxserver/socket-proxy:latest - container_name: socket-proxy + container_name: test-socket-proxy environment: - ALLOW_START=1 #optional - ALLOW_STOP=1 #optional @@ -59,3 +70,5 @@ services: read_only: true tmpfs: - /run + networks: + - shared-network diff --git a/package.json b/package.json index d600a79..eb9f865 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,9 @@ "build": "npx tsc", "build:mini": "npx tsc && bash ./src/misc/minifyDist.sh --build-only", "mini": "bash ./src/misc/minifyDist.sh", - "docker": "sudo docker compose up -d", + "docker": "docker compose up -d", "docker:full": "docker compose up -d && [ -z \"$TMUX\" ] && tmux new-session -d -s docker 'docker compose logs -f master' \\; split-window -v 'docker compose logs -f slave' \\; attach-session || echo 'Already inside a tmux session. Exiting.'; docker compose down", - "docker:build": "docker build . -t \"dockstatapi:local\" && docker compose up -d", + "docker:build": "docker build . -t \"dockstatapi:local\" -f ./Dockerfile-dev && docker compose up -d", "docker:build:full": "npm run docker:build && [ -z \"$TMUX\" ] && tmux new-session -d -s docker 'docker compose up -d && docker compose logs -f master' \\; split-window -v 'docker compose logs -f slave' \\; attach-session || echo 'Already inside a tmux session. Exiting.'; docker compose down" }, "keywords": [], diff --git a/src/config/db.ts b/src/config/db.ts index 9397213..8086135 100644 --- a/src/config/db.ts +++ b/src/config/db.ts @@ -15,12 +15,10 @@ const db: sqlite3.Database = new sqlite3.Database( info TEXT NOT NULL, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP )`, - (tableErr: Error | null) => { - if (tableErr) { - logger.error("Error creating table:", tableErr.message); - } else { - logger.info("Database created / opened successfully"); - } + () => { + logger.info( + "Database created / opened successfully, table is ready.", + ); }, ); } diff --git a/src/config/hostsystem.ts b/src/config/hostsystem.ts index 8b4227f..91e44ed 100644 --- a/src/config/hostsystem.ts +++ b/src/config/hostsystem.ts @@ -53,9 +53,8 @@ function writeUserConf() { backendVersion: version, }; - logger.info("Starting the server..."); logger.info( - `At: ${startDetails.startedAt} - Version: ${startDetails.backendVersion} - Docker: ${installationDetails.inDocker} - Installed as: ${installationDetails.installedBy} - Platform: ${installationDetails.platform} - Arch: ${installationDetails.arch}`, + `Starting at: ${startDetails.startedAt} - Version: ${startDetails.backendVersion} - Docker: ${installationDetails.inDocker} - Installed as: ${installationDetails.installedBy} - Platform: ${installationDetails.platform} - Arch: ${installationDetails.arch}`, ); } diff --git a/src/config/initFiles.ts b/src/config/initFiles.ts new file mode 100644 index 0000000..1f8776a --- /dev/null +++ b/src/config/initFiles.ts @@ -0,0 +1,41 @@ +import { writeFileSync, existsSync } from "fs"; +import logger from "../utils/logger"; +import path from "path"; + +const files = [ + { + path: "./src/data/password.json", + content: JSON.stringify( + { + hash: "", + salt: "", + }, + null, + 2, + ), + }, + { path: "./src/data/states.json", content: "{}" }, + { + path: "./src/data/template.json", + content: JSON.stringify( + { text: "{{name}} is {{state}} on {{hostName}}" }, + null, + 2, + ), + }, + { path: "./src/data/frontendConfiguration.json", content: "[]" }, + { path: "./src/data/usePassword.txt", content: "false" }, +]; + +function initFiles(): void { + files.forEach(({ path: filePath, content }) => { + if (!existsSync(filePath)) { + writeFileSync(filePath, content); + logger.info(`Created: ${filePath}`); + } else { + logger.debug(`Skipped (already exists): ${filePath}`); + } + }); +} + +export default initFiles; diff --git a/src/config/loggerConfig.ts b/src/config/loggerConfig.ts index 5d1a33e..45feb5c 100644 --- a/src/config/loggerConfig.ts +++ b/src/config/loggerConfig.ts @@ -7,6 +7,7 @@ const red = "\x1b[31m"; const green = "\x1b[32m"; const yellow = "\x1b[33m"; const blue = "\x1b[34m"; +const pink = "\x1b[38;5;213m"; // Pink color for sync logs const ignoreExitListenerLogs = format((info) => { if ( diff --git a/src/controllers/highAvailability.ts b/src/controllers/highAvailability.ts index f02bde9..919148e 100644 --- a/src/controllers/highAvailability.ts +++ b/src/controllers/highAvailability.ts @@ -34,7 +34,7 @@ interface NodeCache { const haMasterPath: string = "./src/data/highAvailability.json"; const haNodePath: string = "./src/data/haNode.json"; const nodeCachePath: string = "./src/data/nodeCache.json"; -const useUnsafeConnection: boolean = HA_UNSAFE == "false"; +const useUnsafeConnection: boolean = JSON.parse(HA_UNSAFE || "false"); const lockFilePath: string = "./src/data/ha.lock"; const configFiles: string[] = [ @@ -45,7 +45,6 @@ const configFiles: string[] = [ "./src/data/nodeCache.json", "./src/data/usePassword.txt", "./src/data/password.json", - "./src/data/variables.json", ]; async function acquireLock(): Promise { @@ -130,6 +129,8 @@ async function checkApiReachable(node: string): Promise { ? `http://${node}/api/status` : `https://${node}/api/status`; + logger.info(`Checking node (${nodeUrl}) reachability`); + try { const response = await fetch(nodeUrl); if (!response.ok) { @@ -138,7 +139,7 @@ async function checkApiReachable(node: string): Promise { } const data = await response.json(); - if (data.ApiReachable) { + if (data.ApiReachable as boolean) { logger.info(`Node ${node} is reachable.`); return true; } else { @@ -208,15 +209,14 @@ function monitorConfigFiles(): void { } async function startMasterNode() { - let isMaster: boolean = HA_MASTER == "false"; - if (isMaster) { + if (HA_MASTER == "true") { if (!HA_MASTER_IP) { logger.error( "Master's IP is not set, please set the HA_MASTER_IP variable (example: 10.0.0.4:9876)", ); } else { const haNodeConfig: HaNodeConfig = { - master: "HA_MASTER_IP", + master: HA_MASTER_IP, }; const haConfig: HighAvailabilityConfig = { active: true, @@ -226,9 +226,9 @@ async function startMasterNode() { const nodeCache: NodeCache = HA_NODE ? HA_NODE.split(",").reduce((cache, node, index) => { - const [ip, id] = node.trim().split(":"); - if (ip && id) { - cache[`node${index + 1}`] = { ip, id: parseInt(id, 10) }; + const [ip, port] = node.trim().split(":"); + if (ip && port) { + cache[`node-${index + 1}`] = { ip, id: parseInt(port, 10) }; } return cache; }, {} as NodeCache) diff --git a/src/init.ts b/src/init.ts index eb3612b..119950c 100644 --- a/src/init.ts +++ b/src/init.ts @@ -15,10 +15,12 @@ import { scheduleFetch } from "./controllers/scheduler"; import cors from "cors"; import { blockWhileLocked } from "./middleware/checkLock"; import logger from "./utils/logger"; +import initFiles from "./config/initFiles"; const LAB = [limiter, authMiddleware, blockWhileLocked]; const initializeApp = (app: express.Application): void => { + initFiles(); app.use(cors()); app.use(express.json()); app.use("/api-docs", (req: Request, res: Response, next: NextFunction) => From ccabc0cea19b18a936b17d195276ac2663a518f6 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 25 Dec 2024 23:38:13 +0100 Subject: [PATCH 040/324] Fix: Replacing all single file names with one folder --- .gitignore | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index ee4e7af..6c61786 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,5 @@ # custom paths: -src/data/database.db -src/data/dockerConfig.json -src/data/highAvailability.json -src/data/states.json -src/data/user.conf -src/data/password.json -src/data/ha.lock -src/data/frontendConfiguration.json -src/data/variables.json +src/data/* docker .test* From d9c600abd8a36c696c247d45f11147cf4c8ea55a Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 25 Dec 2024 23:49:09 +0100 Subject: [PATCH 041/324] Fix: Adding sample variables.json to docker container before building --- .dockerignore | 150 ++++++++++++++++++++++++++++++++++++++- Dockerfile | 1 + Dockerfile-dev | 1 + src/sample-variable.json | 22 ++++++ 4 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 src/sample-variable.json diff --git a/.dockerignore b/.dockerignore index 10b44ae..2d99309 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,150 @@ +# custom paths: +src/data/* +*.md *.txt -*.md \ No newline at end of file +docker +.test* +# Created by https://www.toptal.com/developers/gitignore/api/node +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### Node Patch ### +# Serverless Webpack directories +.webpack/ + +# Optional stylelint cache + +# SvelteKit build / generate output +.svelte-kit +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/Dockerfile b/Dockerfile index 53f3b72..26f492b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,6 +23,7 @@ COPY tsconfig.json environment.d.ts package*.json tsconfig.json yarn.lock ./ RUN npm install COPY ./src ./src +RUN mv ./src/sample-variable.json ./src/data/variables.json RUN npm run build:mini # Stage 2: main stage diff --git a/Dockerfile-dev b/Dockerfile-dev index 7ad56f0..6e9452a 100644 --- a/Dockerfile-dev +++ b/Dockerfile-dev @@ -23,6 +23,7 @@ COPY tsconfig.json environment.d.ts package*.json tsconfig.json yarn.lock ./ RUN npm install COPY ./src ./src +RUN mv ./src/sample-variable.json ./src/data/variables.json RUN npm run build # Stage 2: main stage diff --git a/src/sample-variable.json b/src/sample-variable.json new file mode 100644 index 0000000..06153af --- /dev/null +++ b/src/sample-variable.json @@ -0,0 +1,22 @@ +{ + "VERSION": "", + "RUNNING_IN_DOCKER": "", + "TRUSTED_PROXYS": "", + "HA_MASTER": "", + "HA_MASTER_IP": "", + "HA_NODE": "", + "HA_UNSAFE": "", + "DISCORD_WEBHOOK_URL": "", + "EMAIL_SENDER": "", + "EMAIL_RECIPIENT": "", + "EMAIL_PASSWORD": "", + "EMAIL_SERVICE": "", + "PUSHBULLET_ACCESS_TOKEN": "", + "PUSHOVER_USER_KEY": "", + "PUSHOVER_API_TOKEN": "", + "SLACK_WEBHOOK_URL": "", + "TELEGRAM_BOT_TOKEN": "", + "TELEGRAM_CHAT_ID": "", + "WHATSAPP_API_URL": "", + "WHATSAPP_RECIPIENT": "" +} From c82473ecda7b2fca0e5697cd8bc0fa76e517800a Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 26 Dec 2024 10:16:24 +0100 Subject: [PATCH 042/324] Fix: Add error handling for malformed input data in the reduce function Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- src/utils/extractHostData.ts | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/utils/extractHostData.ts b/src/utils/extractHostData.ts index b6192ea..65577cc 100644 --- a/src/utils/extractHostData.ts +++ b/src/utils/extractHostData.ts @@ -43,13 +43,23 @@ function extractRelevantData(jsonData: JsonData) { NCPU: jsonData.info.NCPU, }, version: { - Components: jsonData.version.Components.reduce( - (acc, component) => { - acc[component.Name] = component.Version; - return acc; - }, - {}, - ), + Components: (() => { + try { + if (!Array.isArray(jsonData?.version?.Components)) { + return {}; + } + + return jsonData.version.Components.reduce((acc, component) => { + if (typeof component?.Name === 'string' && typeof component?.Version === 'string') { + acc[component.Name] = component.Version; + } + return acc; + }, {}); + } catch (error) { + console.error('Error processing Components data:', error); + return {}; + } + })(), }, }; } From 35630f46d8fe6f0c017877f928e459fdc6ef39ad Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 26 Dec 2024 18:47:22 +0100 Subject: [PATCH 043/324] Fix: Add rate limiting to file read (auth) --- src/middleware/authMiddleware.ts | 3 ++- src/utils/rateLimitReadFile.ts | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 src/utils/rateLimitReadFile.ts diff --git a/src/middleware/authMiddleware.ts b/src/middleware/authMiddleware.ts index 8caad08..a50fcda 100644 --- a/src/middleware/authMiddleware.ts +++ b/src/middleware/authMiddleware.ts @@ -2,6 +2,7 @@ import bcrypt from "bcrypt"; import fs from "fs"; import { Request, Response, NextFunction } from "express"; import logger from "../utils/logger"; +import { rateLimitedReadFile } from "../utils/rateLimitReadFile"; const passwordFile = "./src/data/password.json"; const passwordBool = "./src/data/usePassword.txt"; @@ -28,7 +29,7 @@ async function authMiddleware( return; } - const passwordData = await fs.promises.readFile(passwordFile, "utf8"); + const passwordData = await rateLimitedReadFile(passwordFile); const storedData = JSON.parse(passwordData); const passwordMatch = await bcrypt.compare( diff --git a/src/utils/rateLimitReadFile.ts b/src/utils/rateLimitReadFile.ts new file mode 100644 index 0000000..415cdde --- /dev/null +++ b/src/utils/rateLimitReadFile.ts @@ -0,0 +1,22 @@ +import { promises as fs } from "fs"; + +const delay = (ms: number): Promise => + new Promise((resolve) => setTimeout(resolve, ms)); + +let lastReadTime = 0; +const rateLimitDuration = 500; + +export const rateLimitedReadFile = async ( + filePath: string, + encoding: BufferEncoding = "utf8", +): Promise => { + const now = Date.now(); + const timeSinceLastRead = now - lastReadTime; + + if (timeSinceLastRead < rateLimitDuration) { + await delay(rateLimitDuration - timeSinceLastRead); + } + + lastReadTime = Date.now(); + return fs.readFile(filePath, encoding); +}; From f81a96cc0bab405d4ee802ecaa4828a1689d9fdc Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 26 Dec 2024 18:58:03 +0100 Subject: [PATCH 044/324] Fix: Add rate limiting FS functions --- src/middleware/authMiddleware.ts | 5 ++--- src/middleware/checkLock.ts | 8 +++---- src/utils/rateLimitFS.ts | 36 ++++++++++++++++++++++++++++++++ src/utils/rateLimitReadFile.ts | 22 ------------------- 4 files changed, 42 insertions(+), 29 deletions(-) create mode 100644 src/utils/rateLimitFS.ts delete mode 100644 src/utils/rateLimitReadFile.ts diff --git a/src/middleware/authMiddleware.ts b/src/middleware/authMiddleware.ts index a50fcda..08ffd21 100644 --- a/src/middleware/authMiddleware.ts +++ b/src/middleware/authMiddleware.ts @@ -1,8 +1,7 @@ import bcrypt from "bcrypt"; -import fs from "fs"; import { Request, Response, NextFunction } from "express"; import logger from "../utils/logger"; -import { rateLimitedReadFile } from "../utils/rateLimitReadFile"; +import { rateLimitedReadFile } from "../utils/rateLimitFS"; const passwordFile = "./src/data/password.json"; const passwordBool = "./src/data/usePassword.txt"; @@ -13,7 +12,7 @@ async function authMiddleware( next: NextFunction, ): Promise { try { - const authStatusData = await fs.promises.readFile(passwordBool, "utf8"); + const authStatusData = await rateLimitedReadFile(passwordBool); const isAuthEnabled = authStatusData.trim() === "true"; if (!isAuthEnabled) { diff --git a/src/middleware/checkLock.ts b/src/middleware/checkLock.ts index 747889d..73740a0 100644 --- a/src/middleware/checkLock.ts +++ b/src/middleware/checkLock.ts @@ -1,14 +1,14 @@ -import fs from "fs"; import { Request, Response, NextFunction } from "express"; +import { rateLimitedExistsSync } from "../utils/rateLimitFS"; const lockFilePath = "./src/data/ha.lock"; -export function blockWhileLocked( +export async function blockWhileLocked( req: Request, res: Response, next: NextFunction, -): void { - if (fs.existsSync(lockFilePath)) { +): Promise { + if (await rateLimitedExistsSync(lockFilePath)) { res.status(503).json({ error: "Service unavailable. The high-availability lock is currently active. Please try again later.", diff --git a/src/utils/rateLimitFS.ts b/src/utils/rateLimitFS.ts new file mode 100644 index 0000000..a8f0b42 --- /dev/null +++ b/src/utils/rateLimitFS.ts @@ -0,0 +1,36 @@ +import { promises as fs, existsSync } from "fs"; + +const delay = (ms: number): Promise => + new Promise((resolve) => setTimeout(resolve, ms)); + +let lastOperationTime = 0; +const rateLimitDuration = 500; + +export const rateLimitedReadFile = async ( + filePath: string, + encoding: BufferEncoding = "utf8", +): Promise => { + const now = Date.now(); + const timeSinceLastOperation = now - lastOperationTime; + + if (timeSinceLastOperation < rateLimitDuration) { + await delay(rateLimitDuration - timeSinceLastOperation); + } + + lastOperationTime = Date.now(); + return fs.readFile(filePath, encoding); +}; + +export const rateLimitedExistsSync = async ( + filePath: string, +): Promise => { + const now = Date.now(); + const timeSinceLastOperation = now - lastOperationTime; + + if (timeSinceLastOperation < rateLimitDuration) { + await delay(rateLimitDuration - timeSinceLastOperation); + } + + lastOperationTime = Date.now(); + return existsSync(filePath); +}; diff --git a/src/utils/rateLimitReadFile.ts b/src/utils/rateLimitReadFile.ts deleted file mode 100644 index 415cdde..0000000 --- a/src/utils/rateLimitReadFile.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { promises as fs } from "fs"; - -const delay = (ms: number): Promise => - new Promise((resolve) => setTimeout(resolve, ms)); - -let lastReadTime = 0; -const rateLimitDuration = 500; - -export const rateLimitedReadFile = async ( - filePath: string, - encoding: BufferEncoding = "utf8", -): Promise => { - const now = Date.now(); - const timeSinceLastRead = now - lastReadTime; - - if (timeSinceLastRead < rateLimitDuration) { - await delay(rateLimitDuration - timeSinceLastRead); - } - - lastReadTime = Date.now(); - return fs.readFile(filePath, encoding); -}; From 8b9493fff416963f97ed152c4514267855d8c434 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 26 Dec 2024 19:16:01 +0100 Subject: [PATCH 045/324] Feat: Advanced codeql.yml --- .github/workflows/codeql.yml | 52 ++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..695a608 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,52 @@ +name: "CodeQL Advanced" + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: '32 1 * * 5' + +jobs: + analyze: + name: Analyze TypeScript + runs-on: 'ubuntu-latest' + permissions: + security-events: write + packages: read + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: javascript-typescript + build-mode: none + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + queries: security-extended + + - if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" From 8165b3e7ac456b6074451a237db46c0343b37b18 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 26 Dec 2024 19:47:17 +0100 Subject: [PATCH 046/324] Chore: Add more workflows for validation/licensing/... --- .github/workflows/Licensed.yml | 12 + .github/workflows/remove-stale.yml | 17 + .github/workflows/validation.yml | 58 + eslint.config.mjs | 12 + package-lock.json | 1359 +++++++++++++++++++++- package.json | 20 +- src/controllers/frontendConfiguration.ts | 50 +- src/controllers/highAvailability.ts | 4 +- src/routes/getter/routes.ts | 2 +- src/utils/connectionChecker.ts | 1 - src/utils/containerService.ts | 12 +- src/utils/extractHostData.ts | 20 +- 12 files changed, 1516 insertions(+), 51 deletions(-) create mode 100644 .github/workflows/Licensed.yml create mode 100644 .github/workflows/remove-stale.yml create mode 100644 .github/workflows/validation.yml create mode 100644 eslint.config.mjs diff --git a/.github/workflows/Licensed.yml b/.github/workflows/Licensed.yml new file mode 100644 index 0000000..e6475a7 --- /dev/null +++ b/.github/workflows/Licensed.yml @@ -0,0 +1,12 @@ +name: Licensed + +on: + workflow_call: + +jobs: + validate-cached-dependency-records: + runs-on: ubuntu-latest + name: Check licenses + steps: + - name: Licensed CI + uses: github/licensed-ci@v1.11.1 diff --git a/.github/workflows/remove-stale.yml b/.github/workflows/remove-stale.yml new file mode 100644 index 0000000..ccccef9 --- /dev/null +++ b/.github/workflows/remove-stale.yml @@ -0,0 +1,17 @@ +name: "Close stale issues and PR" +on: + schedule: + - cron: "30 1 * * *" + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + with: + stale-issue-message: "This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days." + stale-pr-message: "This PR is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 10 days." + close-issue-message: "This issue was closed because it has been stalled for 5 days with no activity." + days-before-stale: 30 + days-before-close: 5 + days-before-pr-close: -1 diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml new file mode 100644 index 0000000..7dcf706 --- /dev/null +++ b/.github/workflows/validation.yml @@ -0,0 +1,58 @@ +name: Basic validation + +on: + workflow_call: + inputs: + operating-systems: + description: "Optional input to set a list of operating systems which the workflow uses. Defaults to ['ubuntu-latest', 'windows-latest', 'macos-latest'] if not set" + required: false + type: string + default: "['ubuntu-latest', 'windows-latest', 'macos-latest']" + enable-audit: + description: "Optional input to enable npm package audit process" + required: false + type: boolean + default: true + node-version: + description: "Optional input to set the version of Node.js used to build the project. The input syntax corresponds to the setup-node's one" + required: false + type: string + default: "20.x" + node-caching: + description: "Optional input to set up caching for the setup-node action. The input syntax corresponds to the setup-node's one. Set to an empty string if caching isn't needed" + required: false + type: string + default: "npm" + +jobs: + build: + runs-on: ${{matrix.operating-systems}} + strategy: + fail-fast: false + matrix: + operating-systems: ${{fromJson(inputs.operating-systems)}} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js ${{inputs.node-version}} + uses: actions/setup-node@v4 + with: + node-version: ${{inputs.node-version}} + cache: ${{inputs.node-caching}} + + - name: Install dependencies + run: npm ci --ignore-scripts + + - name: Run prettier + run: npm run pettier + + - name: Run linter + run: npm run lint + + - name: Build + run: npm run build:mini + + - name: Audit packages + run: npm audit --audit-level=high + if: ${{inputs.enable-audit}} diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..5b7b70a --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,12 @@ +import globals from "globals"; +import pluginJs from "@eslint/js"; +import tseslint from "typescript-eslint"; + +/** @type {import('eslint').Linter.Config[]} */ +export default [ + { ignores: ["node_modules/*", "dist/*"] }, + { files: ["src/*.{ts}"] }, + { languageOptions: { globals: globals.node } }, + pluginJs.configs.recommended, + ...tseslint.configs.recommended, +]; diff --git a/package-lock.json b/package-lock.json index 27899c7..118aa2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "dockstatapi", - "version": "2.0.0", + "version": "2.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dockstatapi", - "version": "2.0.0", + "version": "2.0.1", "license": "BSD 3-Clause License", "dependencies": { "bcrypt": "^5.1.1", @@ -25,6 +25,7 @@ "winston": "^3.15.0" }, "devDependencies": { + "@eslint/js": "^9.17.0", "@playwright/test": "^1.49.0", "@types/bcrypt": "^5.0.2", "@types/cors": "^2.8.17", @@ -37,11 +38,17 @@ "@types/supports-color": "^8.1.3", "@types/swagger-jsdoc": "^6.0.4", "@types/swagger-ui-express": "^4.1.7", + "@typescript-eslint/eslint-plugin": "^8.18.2", + "@typescript-eslint/parser": "^8.18.2", "dependency-cruiser": "^16.5.0", + "eslint": "^9.17.0", + "globals": "^15.14.0", "nodemon": "^3.1.7", "ora": "^8.1.1", + "prettier": "^3.4.2", "ts-node": "^10.9.2", "tsx": "^4.19.2", + "typescript-eslint": "^8.18.2", "uglify-js": "^3.19.3" }, "engines": { @@ -539,6 +546,180 @@ "node": ">=18" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", + "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz", + "integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.5", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.1.tgz", + "integrity": "sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", + "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.17.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.17.0.tgz", + "integrity": "sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz", + "integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.4.tgz", + "integrity": "sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -546,6 +727,72 @@ "license": "MIT", "optional": true }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", + "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -620,6 +867,44 @@ } } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@npmcli/fs": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", @@ -771,6 +1056,13 @@ "@types/ssh2": "*" } }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/express": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", @@ -950,6 +1242,235 @@ "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", "license": "MIT" }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.2.tgz", + "integrity": "sha512-adig4SzPLjeQ0Tm+jvsozSGiCliI2ajeURDGHjZ2llnA+A67HihCQ+a3amtPhUakd1GlwHxSRvzOZktbEvhPPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.18.2", + "@typescript-eslint/type-utils": "8.18.2", + "@typescript-eslint/utils": "8.18.2", + "@typescript-eslint/visitor-keys": "8.18.2", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.18.2.tgz", + "integrity": "sha512-y7tcq4StgxQD4mDr9+Jb26dZ+HTZ/SkfqpXSiqeUXZHxOUyjWDKsmwKhJ0/tApR08DgOhrFAoAhyB80/p3ViuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.18.2", + "@typescript-eslint/types": "8.18.2", + "@typescript-eslint/typescript-estree": "8.18.2", + "@typescript-eslint/visitor-keys": "8.18.2", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.18.2.tgz", + "integrity": "sha512-YJFSfbd0CJjy14r/EvWapYgV4R5CHzptssoag2M7y3Ra7XNta6GPAJPPP5KGB9j14viYXyrzRO5GkX7CRfo8/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.18.2", + "@typescript-eslint/visitor-keys": "8.18.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.18.2.tgz", + "integrity": "sha512-AB/Wr1Lz31bzHfGm/jgbFR0VB0SML/hd2P1yxzKDM48YmP7vbyJNHRExUE/wZsQj2wUCvbWH8poNHFuxLqCTnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.18.2", + "@typescript-eslint/utils": "8.18.2", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.18.2.tgz", + "integrity": "sha512-Z/zblEPp8cIvmEn6+tPDIHUbRu/0z5lqZ+NvolL5SvXWT5rQy7+Nch83M0++XzO0XrWRFWECgOAyE8bsJTl1GQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.2.tgz", + "integrity": "sha512-WXAVt595HjpmlfH4crSdM/1bcsqh+1weFRWIa9XMTx/XHZ9TCKMcr725tLYqWOgzKdeDrqVHxFotrvWcEsk2Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.18.2", + "@typescript-eslint/visitor-keys": "8.18.2", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.18.2.tgz", + "integrity": "sha512-Cr4A0H7DtVIPkauj4sTSXVl+VBWewE9/o40KcF3TV9aqDEOWoXF3/+oRXNby3DYzZeCATvbdksYsGZzplwnK/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.18.2", + "@typescript-eslint/types": "8.18.2", + "@typescript-eslint/typescript-estree": "8.18.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.2.tgz", + "integrity": "sha512-zORcwn4C3trOWiCqFQP1x6G3xTRyZ1LYydnj51cRnJ6hxBlr/cKPckk+PKPUw/fXmvfKTcw7bwY3w9izgx5jZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.18.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -1467,6 +1988,16 @@ "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", "license": "MIT" }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/chalk": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", @@ -1702,6 +2233,21 @@ "dev": true, "license": "MIT" }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -1752,6 +2298,13 @@ "node": ">=4.0.0" } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -2074,6 +2627,276 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.17.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.17.0.tgz", + "integrity": "sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.19.0", + "@eslint/core": "^0.9.0", + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "9.17.0", + "@eslint/plugin-kit": "^0.2.3", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.1", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", + "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -2184,6 +3007,37 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-uri": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", @@ -2191,6 +3045,16 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/fastq": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", + "integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fecha": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", @@ -2220,6 +3084,19 @@ "node": "^12.20 || >= 14.13" } }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -2272,6 +3149,44 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", + "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", + "dev": true, + "license": "ISC" + }, "node_modules/fn.name": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", @@ -2496,6 +3411,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globals": { + "version": "15.14.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz", + "integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -2515,6 +3443,13 @@ "devOptional": true, "license": "ISC" }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -2683,12 +3618,29 @@ "dev": true, "license": "ISC" }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "devOptional": true, "license": "MIT", - "optional": true, "engines": { "node": ">=0.8.19" } @@ -2926,8 +3878,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC", - "optional": true + "devOptional": true, + "license": "ISC" }, "node_modules/js-yaml": { "version": "4.1.0", @@ -2948,6 +3900,13 @@ "license": "MIT", "optional": true }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -2955,6 +3914,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -2968,6 +3934,16 @@ "node": ">=6" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -2984,6 +3960,36 @@ "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", "license": "MIT" }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", @@ -2996,6 +4002,13 @@ "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", "license": "MIT" }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.mergewith": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", @@ -3155,6 +4168,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -3164,6 +4187,33 @@ "node": ">= 0.6" } }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -3375,6 +4425,13 @@ "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", "license": "MIT" }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -3718,6 +4775,24 @@ "license": "MIT", "peer": true }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/ora": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/ora/-/ora-8.1.1.tgz", @@ -3796,6 +4871,38 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-map": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", @@ -3812,6 +4919,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -3821,6 +4941,16 @@ "node": ">= 0.8" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -3830,6 +4960,16 @@ "node": ">=0.10.0" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -3921,6 +5061,32 @@ "node": ">=10" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -3995,6 +5161,16 @@ "once": "^1.3.1" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", @@ -4010,6 +5186,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -4133,6 +5330,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -4183,6 +5390,17 @@ "node": ">= 4" } }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -4199,6 +5417,30 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -4348,6 +5590,29 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/side-channel": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", @@ -4887,6 +6152,19 @@ "node": ">= 14.0.0" } }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", @@ -5055,6 +6333,19 @@ "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", "license": "Unlicense" }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -5083,6 +6374,29 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.18.2.tgz", + "integrity": "sha512-KuXezG6jHkvC3MvizeXgupZzaG5wjhU3yE8E7e6viOvAvD9xAWYp8/vy0WULTGe9DYDWcQu7aW03YIV3mSitrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.18.2", + "@typescript-eslint/parser": "8.18.2", + "@typescript-eslint/utils": "8.18.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, "node_modules/uglify-js": { "version": "3.19.3", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", @@ -5139,6 +6453,16 @@ "node": ">= 0.8" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -5221,8 +6545,8 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "devOptional": true, "license": "ISC", - "optional": true, "dependencies": { "isexe": "^2.0.0" }, @@ -5278,6 +6602,16 @@ "node": ">= 12.0.0" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -5309,6 +6643,19 @@ "node": ">=6" } }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/z-schema": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", diff --git a/package.json b/package.json index eb9f865..20af78a 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,10 @@ "docker": "docker compose up -d", "docker:full": "docker compose up -d && [ -z \"$TMUX\" ] && tmux new-session -d -s docker 'docker compose logs -f master' \\; split-window -v 'docker compose logs -f slave' \\; attach-session || echo 'Already inside a tmux session. Exiting.'; docker compose down", "docker:build": "docker build . -t \"dockstatapi:local\" -f ./Dockerfile-dev && docker compose up -d", - "docker:build:full": "npm run docker:build && [ -z \"$TMUX\" ] && tmux new-session -d -s docker 'docker compose up -d && docker compose logs -f master' \\; split-window -v 'docker compose logs -f slave' \\; attach-session || echo 'Already inside a tmux session. Exiting.'; docker compose down" + "docker:build:full": "npm run docker:build && [ -z \"$TMUX\" ] && tmux new-session -d -s docker 'docker compose up -d && docker compose logs -f master' \\; split-window -v 'docker compose logs -f slave' \\; attach-session || echo 'Already inside a tmux session. Exiting.'; docker compose down", + "prettier": "npx prettier -c ./src/**/*.ts --parser typescript --write", + "lint": "npx eslint", + "lint:fix": "npx eslint --fix" }, "keywords": [], "author": "Its4Nik", @@ -39,23 +42,30 @@ "winston": "^3.15.0" }, "devDependencies": { - "@types/dockerode": "^3.3.31", - "@types/supports-color": "^8.1.3", - "@types/swagger-jsdoc": "^6.0.4", - "@types/swagger-ui-express": "^4.1.7", + "@eslint/js": "^9.17.0", "@playwright/test": "^1.49.0", "@types/bcrypt": "^5.0.2", "@types/cors": "^2.8.17", + "@types/dockerode": "^3.3.31", "@types/express": "^5.0.0", "@types/express-handlebars": "^5.3.1", "@types/node": "^22.9.0", "@types/node-fetch": "^2.6.12", "@types/nodemailer": "^6.4.17", + "@types/supports-color": "^8.1.3", + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.7", + "@typescript-eslint/eslint-plugin": "^8.18.2", + "@typescript-eslint/parser": "^8.18.2", "dependency-cruiser": "^16.5.0", + "eslint": "^9.17.0", + "globals": "^15.14.0", "nodemon": "^3.1.7", "ora": "^8.1.1", + "prettier": "^3.4.2", "ts-node": "^10.9.2", "tsx": "^4.19.2", + "typescript-eslint": "^8.18.2", "uglify-js": "^3.19.3" }, "engines": { diff --git a/src/controllers/frontendConfiguration.ts b/src/controllers/frontendConfiguration.ts index e83eaae..4d31943 100644 --- a/src/controllers/frontendConfiguration.ts +++ b/src/controllers/frontendConfiguration.ts @@ -9,7 +9,7 @@ const regex = new RegExp(expression); // Hide Containers: async function hideContainer(containerName: string) { try { - let data = await readData(); + const data = await readData(); const containerIndex = data.findIndex( (container: any) => container.name === containerName, ); @@ -29,7 +29,7 @@ async function hideContainer(containerName: string) { async function unhideContainer(containerName: string) { try { - let data = await readData(); + const data = await readData(); const containerIndex = data.findIndex( (container: any) => container.name === containerName, ); @@ -49,7 +49,7 @@ async function unhideContainer(containerName: string) { // Tag containers async function addTagToContainer(containerName: string, tag: string) { try { - let data = await readData(); + const data = await readData(); const containerIndex = data.findIndex( (container: any) => container.name === containerName, ); @@ -72,7 +72,7 @@ async function addTagToContainer(containerName: string, tag: string) { async function removeTagFromContainer(containerName: string, tag: string) { try { - let data = await readData(); + const data = await readData(); const containerIndex = data.findIndex( (container: any) => container.name === containerName, ); @@ -94,7 +94,7 @@ async function removeTagFromContainer(containerName: string, tag: string) { // Pin containers async function pinContainer(containerName: string) { try { - let data: any = await readData(); + const data: any = await readData(); const containerIndex: number = data.findIndex( (container: any) => container.name === containerName, ); @@ -114,7 +114,7 @@ async function pinContainer(containerName: string) { async function unpinContainer(containerName: string) { try { - let data = await readData(); + const data = await readData(); const containerIndex = data.findIndex( (container: any) => container.name === containerName, ); @@ -135,7 +135,7 @@ async function unpinContainer(containerName: string) { async function setLink(containerName: string, link: string) { if (link.match(regex)) { try { - let data: any = await readData(); + const data: any = await readData(); const containerIndex: any = data.findIndex( (container: any) => container.name === containerName, ); @@ -159,7 +159,7 @@ async function setLink(containerName: string, link: string) { async function removeLink(containerName: string) { try { - let data = await readData(); + const data = await readData(); const containerIndex = data.findIndex( (container: any) => container.name === containerName, ); @@ -179,28 +179,26 @@ async function removeLink(containerName: string) { // Add/remove icon from containers async function setIcon(containerName: string, icon: string, custom: boolean) { try { - let data = await readData(); + const data = await readData(); const containerIndex: number = data.findIndex( (container: any) => container.name === containerName, ); if (custom === true) { - if (containerIndex !== -1) { - data[containerIndex].icon = `custom/${icon}`; - await saveData(data); - } else { - data.push({ name: containerName, icon: `custom/${icon}` }); - await saveData(data); - } - } - else if (containerIndex !== -1) { - data[containerIndex].icon = `${icon}`; - await saveData(data); - } - else { - data.push({ name: containerName, icon: `${icon}` }); - await saveData(data); - } + if (containerIndex !== -1) { + data[containerIndex].icon = `custom/${icon}`; + await saveData(data); + } else { + data.push({ name: containerName, icon: `custom/${icon}` }); + await saveData(data); + } + } else if (containerIndex !== -1) { + data[containerIndex].icon = `${icon}`; + await saveData(data); + } else { + data.push({ name: containerName, icon: `${icon}` }); + await saveData(data); + } } catch (error: any) { logger.error(error); throw new Error(error); @@ -209,7 +207,7 @@ async function setIcon(containerName: string, icon: string, custom: boolean) { async function removeIcon(containerName: string) { try { - let data = await readData(); + const data = await readData(); const containerIndex = data.findIndex( (container: any) => container.name === containerName, ); diff --git a/src/controllers/highAvailability.ts b/src/controllers/highAvailability.ts index 919148e..dd16bf6 100644 --- a/src/controllers/highAvailability.ts +++ b/src/controllers/highAvailability.ts @@ -124,7 +124,7 @@ async function prepareFilesForSync(): Promise> { } async function checkApiReachable(node: string): Promise { - let nodeUrl = + const nodeUrl = useUnsafeConnection === true ? `http://${node}/api/status` : `https://${node}/api/status`; @@ -170,7 +170,7 @@ async function synchronizeFilesWithNodes(): Promise { continue; // Skip synchronization if the node is unreachable } - let nodeUrl = + const nodeUrl = useUnsafeConnection == true ? `http://${node}/ha/sync` : `https://${node}/ha/sync`; diff --git a/src/routes/getter/routes.ts b/src/routes/getter/routes.ts index b6c89c1..8e3c695 100644 --- a/src/routes/getter/routes.ts +++ b/src/routes/getter/routes.ts @@ -332,7 +332,7 @@ router.get("/current-schedule", (req: Request, res: Response) => { router.get("/status", async (req: Request, res: Response) => { logger.debug("Fetching /api/status"); try { - let jsonData = await checkReachability(); + const jsonData = await checkReachability(); res.status(200).json(jsonData); } catch (error: any) { logger.error(`Error while fetching data: ${error}`); diff --git a/src/utils/connectionChecker.ts b/src/utils/connectionChecker.ts index c61ffeb..289b9b3 100644 --- a/src/utils/connectionChecker.ts +++ b/src/utils/connectionChecker.ts @@ -67,7 +67,6 @@ async function checkReachability(): Promise { const parsedData = JSON.parse(data); const hosts: Host[] = parsedData.hosts; return await checkHostStatus(hosts); - } catch (error: any) { logger.error(`Error reading file: ${error}`); return undefined; diff --git a/src/utils/containerService.ts b/src/utils/containerService.ts index afc035a..0cd09e3 100644 --- a/src/utils/containerService.ts +++ b/src/utils/containerService.ts @@ -31,8 +31,14 @@ interface AllContainerData { function loadConfig() { try { if (!fs.existsSync(configPath)) { - logger.warn(`Config file not found. Creating an empty file at ${configPath}`); - fs.writeFileSync(configPath, JSON.stringify({ "hosts": [] }, null, 2), "utf-8"); + logger.warn( + `Config file not found. Creating an empty file at ${configPath}`, + ); + fs.writeFileSync( + configPath, + JSON.stringify({ hosts: [] }, null, 2), + "utf-8", + ); } const configData = fs.readFileSync(configPath, "utf-8"); @@ -80,7 +86,7 @@ async function fetchAllContainers(): Promise { const cpuUsage = systemCpuDelta > 0 ? (cpuDelta / systemCpuDelta) * - containerStats.cpu_stats.online_cpus + containerStats.cpu_stats.online_cpus : 0; return { diff --git a/src/utils/extractHostData.ts b/src/utils/extractHostData.ts index 65577cc..25ea016 100644 --- a/src/utils/extractHostData.ts +++ b/src/utils/extractHostData.ts @@ -49,14 +49,20 @@ function extractRelevantData(jsonData: JsonData) { return {}; } - return jsonData.version.Components.reduce((acc, component) => { - if (typeof component?.Name === 'string' && typeof component?.Version === 'string') { - acc[component.Name] = component.Version; - } - return acc; - }, {}); + return jsonData.version.Components.reduce( + (acc, component) => { + if ( + typeof component?.Name === "string" && + typeof component?.Version === "string" + ) { + acc[component.Name] = component.Version; + } + return acc; + }, + {}, + ); } catch (error) { - console.error('Error processing Components data:', error); + console.error("Error processing Components data:", error); return {}; } })(), From 0dd818e3d210d7ffd4fcacf14c3e8f0477f23882 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 26 Dec 2024 19:55:37 +0100 Subject: [PATCH 047/324] Fix: Update Licensed.yml --- .github/workflows/Licensed.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/Licensed.yml b/.github/workflows/Licensed.yml index e6475a7..192ec8f 100644 --- a/.github/workflows/Licensed.yml +++ b/.github/workflows/Licensed.yml @@ -1,8 +1,9 @@ name: Licensed -on: - workflow_call: - +on: + push: + branches: '**' + jobs: validate-cached-dependency-records: runs-on: ubuntu-latest From d2d65c627b8bb6344052432792d496b0b4db5b0d Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 26 Dec 2024 22:02:40 +0100 Subject: [PATCH 048/324] Fix: Adjusted workflows --- .github/workflows/Licensed.yml | 23 +++++++++++++----- .github/workflows/anchore.yml | 12 ---------- .github/workflows/cloc.yaml | 24 +++++++++---------- .github/workflows/test-build.yaml | 5 +--- .github/workflows/validation.yml | 39 ++++--------------------------- 5 files changed, 35 insertions(+), 68 deletions(-) diff --git a/.github/workflows/Licensed.yml b/.github/workflows/Licensed.yml index e6475a7..b81989f 100644 --- a/.github/workflows/Licensed.yml +++ b/.github/workflows/Licensed.yml @@ -1,12 +1,23 @@ name: Licensed -on: - workflow_call: +on: [push] jobs: - validate-cached-dependency-records: + license-check: runs-on: ubuntu-latest - name: Check licenses steps: - - name: Licensed CI - uses: github/licensed-ci@v1.11.1 + - name: Checkout latest code + uses: actions/checkout@v4 + - name: Use Node.js 20.x + uses: actions/setup-node@latest + with: + node-version: 20.x + - name: Run npm install + run: npm install + - name: Check licenses + uses: tangro/actions-license-check@v1.0.14 + with: + allowed-licenses: "MIT; ISC; Apache-2.0; Custom: https://www.telerik.com/kendo-angular-ui/; Custom: https://www.telerik.com/kendo-react-ui/; BSD" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_CONTEXT: ${{ toJson(github) }} diff --git a/.github/workflows/anchore.yml b/.github/workflows/anchore.yml index bb5df12..23c78ab 100644 --- a/.github/workflows/anchore.yml +++ b/.github/workflows/anchore.yml @@ -1,21 +1,9 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -# This workflow checks out code, builds an image, performs a container image -# vulnerability scan with Anchore's Grype tool, and integrates the results with GitHub Advanced Security -# code scanning feature. For more information on the Anchore scan action usage -# and parameters, see https://github.com/anchore/scan-action. For more -# information on Anchore's container image scanning tool Grype, see -# https://github.com/anchore/grype name: Anchore Grype vulnerability scan on: push: branches: ["main"] pull_request: - # The branches below must be a subset of the branches above branches: ["main", "dev"] schedule: - cron: "30 9 * * 1" diff --git a/.github/workflows/cloc.yaml b/.github/workflows/cloc.yaml index 9ce7e27..795ad75 100644 --- a/.github/workflows/cloc.yaml +++ b/.github/workflows/cloc.yaml @@ -6,23 +6,23 @@ permissions: on: push: - branches: [ main ] + branches: [main, dev] pull_request: - branches: [ main, dev ] + branches: [main, dev] jobs: cloc: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v3 - - name: Count Lines of Code (cloc) - uses: djdefi/cloc-action@6 - with: - options: --md --report-file=cloc.md --exclude-dir=node_modules --exclude-lang=YAML,JSON --exclude-list-file=package-lock.json - - - name: Create comment from markdown file - uses: GrantBirki/comment@v2.1.0 - with: - file: cloc.md \ No newline at end of file + - name: Count Lines of Code (cloc) + uses: djdefi/cloc-action@6 + with: + options: --md --report-file=cloc.md --exclude-dir=node_modules --exclude-lang=YAML,JSON --exclude-list-file=package-lock.json + + - name: Create comment from markdown file + uses: GrantBirki/comment@v2.1.0 + with: + file: cloc.md diff --git a/.github/workflows/test-build.yaml b/.github/workflows/test-build.yaml index 8c805d4..0b304ec 100644 --- a/.github/workflows/test-build.yaml +++ b/.github/workflows/test-build.yaml @@ -1,9 +1,6 @@ name: Test building -on: - pull_request: - branches: - - "dev" +on: [push] permissions: packages: write diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml index 7dcf706..c5150a1 100644 --- a/.github/workflows/validation.yml +++ b/.github/workflows/validation.yml @@ -1,45 +1,17 @@ -name: Basic validation - -on: - workflow_call: - inputs: - operating-systems: - description: "Optional input to set a list of operating systems which the workflow uses. Defaults to ['ubuntu-latest', 'windows-latest', 'macos-latest'] if not set" - required: false - type: string - default: "['ubuntu-latest', 'windows-latest', 'macos-latest']" - enable-audit: - description: "Optional input to enable npm package audit process" - required: false - type: boolean - default: true - node-version: - description: "Optional input to set the version of Node.js used to build the project. The input syntax corresponds to the setup-node's one" - required: false - type: string - default: "20.x" - node-caching: - description: "Optional input to set up caching for the setup-node action. The input syntax corresponds to the setup-node's one. Set to an empty string if caching isn't needed" - required: false - type: string - default: "npm" +on: [push] jobs: build: - runs-on: ${{matrix.operating-systems}} - strategy: - fail-fast: false - matrix: - operating-systems: ${{fromJson(inputs.operating-systems)}} + runs-on: ubuntu steps: - name: Checkout uses: actions/checkout@v4 - - name: Setup Node.js ${{inputs.node-version}} + - name: Setup Node.js 20 uses: actions/setup-node@v4 with: - node-version: ${{inputs.node-version}} - cache: ${{inputs.node-caching}} + node-version: 20 + cache: npm - name: Install dependencies run: npm ci --ignore-scripts @@ -55,4 +27,3 @@ jobs: - name: Audit packages run: npm audit --audit-level=high - if: ${{inputs.enable-audit}} From 575d8e594173c7a04b6828fd759a9e9640b8aeae Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 26 Dec 2024 22:32:28 +0100 Subject: [PATCH 049/324] Fix: Adjusted workflows; fixed versions --- .github/workflows/anchore.yml | 32 ++++------------- .github/workflows/build-dev.yaml | 10 +++--- .github/workflows/build-image.yml | 12 +++---- .github/workflows/build-test.yaml | 56 ++++++++++++++++++++++++++++++ .github/workflows/cloc.yaml | 6 ++-- .github/workflows/codeql.yml | 6 ++-- .github/workflows/licensed.yml | 23 ++++++++++++ .github/workflows/remove-stale.yml | 2 +- .github/workflows/validation.yml | 2 +- 9 files changed, 105 insertions(+), 44 deletions(-) create mode 100644 .github/workflows/build-test.yaml create mode 100644 .github/workflows/licensed.yml diff --git a/.github/workflows/anchore.yml b/.github/workflows/anchore.yml index 715be20..bafb5cc 100644 --- a/.github/workflows/anchore.yml +++ b/.github/workflows/anchore.yml @@ -1,36 +1,18 @@ name: Anchore Grype vulnerability scan -on: - push: - branches: ["main"] - pull_request: - branches: ["main", "dev"] - schedule: - - cron: "30 9 * * 1" - -permissions: - contents: read - +on: [push] jobs: - Anchore-Build-Scan: - permissions: - contents: read - security-events: write # for github/codeql-action/upload-sarif to upload SARIF results - actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status + build: runs-on: ubuntu-latest steps: - - name: Check out the code - uses: actions/checkout@4 - - name: Build the Docker image + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Build the Container image run: docker build . --file Dockerfile --tag localbuild/testimage:latest - - name: Run the Anchore Grype scan action - uses: anchore/scan-action@d5aa5b6cb9414b0c7771438046ff5bcfa2854ed7 + - uses: anchore/scan-action@v3 id: scan with: image: "localbuild/testimage:latest" - fail-build: true - severity-cutoff: critical - - name: Upload vulnerability report - uses: github/codeql-action/upload-sarif + - name: upload Anchore scan SARIF report + uses: github/codeql-action/upload-sarif@v3 with: sarif_file: ${{ steps.scan.outputs.sarif }} diff --git a/.github/workflows/build-dev.yaml b/.github/workflows/build-dev.yaml index f08ba20..b81287c 100644 --- a/.github/workflows/build-dev.yaml +++ b/.github/workflows/build-dev.yaml @@ -14,20 +14,20 @@ jobs: runs-on: ubuntu-latest steps: - name: Set up QEMU - uses: docker/setup-qemu-action + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action + uses: docker/setup-buildx-action@v3 - name: Login to Github Container Registry - uses: docker/login-action + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ github.token }} - name: Generate Docker tags - uses: docker/metadata-action + uses: docker/metadata-action@v5 id: metadata with: images: ghcr.io/${{ github.repository }} @@ -37,7 +37,7 @@ jobs: latest=false - name: Build and push - uses: docker/build-push-action + uses: docker/build-push-action@v6 with: platforms: linux/amd64,linux/arm64, push: true diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index 8c58d70..d7d131e 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -1,4 +1,4 @@ -name: Buiod dockstatapi:latest +name: Build dockstatapi:latest on: release: @@ -13,20 +13,20 @@ jobs: runs-on: ubuntu-latest steps: - name: Set up QEMU - uses: docker/setup-qemu-action + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action + uses: docker/setup-buildx-action@v3 - name: Login to Github Container Registry - uses: docker/login-action + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ github.token }} - name: Generate Docker tags - uses: docker/metadata-action + uses: docker/metadata-action@v5 id: metadata with: images: ghcr.io/${{ github.repository }} @@ -36,7 +36,7 @@ jobs: latest=true - name: Build and push - uses: docker/build-push-action + uses: docker/build-push-action@v6 with: platforms: linux/amd64,linux/arm64 push: true diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml new file mode 100644 index 0000000..8b0af36 --- /dev/null +++ b/.github/workflows/build-test.yaml @@ -0,0 +1,56 @@ +name: Build test docker image + +on: [push] + +permissions: + packages: write + contents: read + +jobs: + build-main: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@4 + + # Set up Node.js using nvm + - name: Set up Node.js version from .nvmrc + run: | + curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash + export NVM_DIR="$HOME/.nvm" + [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" + nvm install + nvm use + node -v + npm -v + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Github Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Generate Docker tags + uses: docker/metadata-action@v5 + id: metadata + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=raw,enable=true,priority=200,prefix=,suffix=,value=${{ github.sha }} + + - name: Build and Push Docker Images + uses: docker/build-push-action@v6 + with: + platforms: linux/amd64,linux/arm64 + push: false + tags: ${{ steps.metadata.outputs.tags }} + labels: ${{ steps.metadata.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/cloc.yaml b/.github/workflows/cloc.yaml index 4c88763..052ba12 100644 --- a/.github/workflows/cloc.yaml +++ b/.github/workflows/cloc.yaml @@ -15,14 +15,14 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@4 + - uses: actions/checkout@v4 - name: Count Lines of Code (cloc) - uses: djdefi/cloc-action + uses: djdefi/cloc-action@6 with: options: --md --report-file=cloc.md --exclude-dir=node_modules --exclude-lang=YAML,JSON --exclude-list-file=package-lock.json - name: Create comment from markdown file - uses: GrantBirki/comment + uses: GrantBirki/comment@v2 with: file: cloc.md diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 3a14993..b1af0a7 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -27,10 +27,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@4 + uses: actions/checkout@v4 - name: Initialize CodeQL - uses: github/codeql-action/init + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -47,6 +47,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze + uses: github/codeql-action/analyze@v3 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/licensed.yml b/.github/workflows/licensed.yml new file mode 100644 index 0000000..f384e7a --- /dev/null +++ b/.github/workflows/licensed.yml @@ -0,0 +1,23 @@ +name: License checker + +on: [push] + +jobs: + license-check: + runs-on: ubuntu-latest + steps: + - name: Checkout latest code + uses: actions/checkout@4 + - name: Use Node.js 20.x + uses: actions/setup-node@v4 + with: + node-version: 20.x + - name: Run npm install + run: npm install + - name: Check licenses + uses: tangro/actions-license-check@v1 + with: + allowed-licenses: "MIT; ISC; Apache-2.0; Custom: https://www.telerik.com/kendo-angular-ui/; Custom: https://www.telerik.com/kendo-react-ui/; BSD" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_CONTEXT: ${{ toJson(github) }} diff --git a/.github/workflows/remove-stale.yml b/.github/workflows/remove-stale.yml index 2285ecb..ccccef9 100644 --- a/.github/workflows/remove-stale.yml +++ b/.github/workflows/remove-stale.yml @@ -7,7 +7,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale + - uses: actions/stale@v9 with: stale-issue-message: "This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days." stale-pr-message: "This PR is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 10 days." diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml index 7402102..20bc3ef 100644 --- a/.github/workflows/validation.yml +++ b/.github/workflows/validation.yml @@ -8,7 +8,7 @@ jobs: uses: actions/checkout@4 - name: Setup Node.js 20 - uses: actions/setup-node + uses: actions/setup-node@v4 with: node-version: 20 cache: npm From 491171d78df3f297537dfae3c6ecb4bbc5b5fd70 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 26 Dec 2024 22:42:56 +0100 Subject: [PATCH 050/324] Fix: Correct workflow versions --- .github/workflows/anchore.yml | 1 + .github/workflows/build-test.yaml | 2 +- .github/workflows/cloc.yaml | 1 + .github/workflows/license.yml | 23 +++++++++++++++++++++++ .github/workflows/validation.yml | 6 ++++-- 5 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/license.yml diff --git a/.github/workflows/anchore.yml b/.github/workflows/anchore.yml index bafb5cc..22632af 100644 --- a/.github/workflows/anchore.yml +++ b/.github/workflows/anchore.yml @@ -12,6 +12,7 @@ jobs: id: scan with: image: "localbuild/testimage:latest" + fail-build: false - name: upload Anchore scan SARIF report uses: github/codeql-action/upload-sarif@v3 with: diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index 8b0af36..f25efed 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@4 + uses: actions/checkout@v4 # Set up Node.js using nvm - name: Set up Node.js version from .nvmrc diff --git a/.github/workflows/cloc.yaml b/.github/workflows/cloc.yaml index 052ba12..9ea3054 100644 --- a/.github/workflows/cloc.yaml +++ b/.github/workflows/cloc.yaml @@ -26,3 +26,4 @@ jobs: uses: GrantBirki/comment@v2 with: file: cloc.md + issue-number: ${{ github.event.number }} diff --git a/.github/workflows/license.yml b/.github/workflows/license.yml new file mode 100644 index 0000000..495314c --- /dev/null +++ b/.github/workflows/license.yml @@ -0,0 +1,23 @@ +name: License checker + +on: [push] + +jobs: + license-check: + runs-on: ubuntu-latest + steps: + - name: Checkout latest code + uses: actions/checkout@v4 + - name: Use Node.js 20.x + uses: actions/setup-node@v4 + with: + node-version: 20.x + - name: Run npm install + run: npm install + - name: Check licenses + uses: tangro/actions-license-check@1 + with: + allowed-licenses: "MIT; ISC; Apache-2.0; Custom: https://www.telerik.com/kendo-angular-ui/; Custom: https://www.telerik.com/kendo-react-ui/; BSD" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_CONTEXT: ${{ toJson(github) }} diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml index 20bc3ef..3183f54 100644 --- a/.github/workflows/validation.yml +++ b/.github/workflows/validation.yml @@ -1,11 +1,13 @@ +name: "Run all tests" + on: [push] jobs: build: - runs-on: ubuntu + runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@4 + uses: actions/checkout@v4 - name: Setup Node.js 20 uses: actions/setup-node@v4 From f4c1c5e0876cefd5541b78e10ffb5f70e197d26d Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 26 Dec 2024 22:48:34 +0100 Subject: [PATCH 051/324] Fix: Workflow stuff... --- .github/workflows/cloc.yaml | 2 -- .github/workflows/license.yml | 2 +- .github/workflows/validation.yml | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cloc.yaml b/.github/workflows/cloc.yaml index 9ea3054..d29afa4 100644 --- a/.github/workflows/cloc.yaml +++ b/.github/workflows/cloc.yaml @@ -5,8 +5,6 @@ permissions: pull-requests: write on: - push: - branches: [main, dev] pull_request: branches: [main, dev] diff --git a/.github/workflows/license.yml b/.github/workflows/license.yml index 495314c..3089a54 100644 --- a/.github/workflows/license.yml +++ b/.github/workflows/license.yml @@ -15,7 +15,7 @@ jobs: - name: Run npm install run: npm install - name: Check licenses - uses: tangro/actions-license-check@1 + uses: tangro/actions-license-check@v1.0.14 with: allowed-licenses: "MIT; ISC; Apache-2.0; Custom: https://www.telerik.com/kendo-angular-ui/; Custom: https://www.telerik.com/kendo-react-ui/; BSD" env: diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml index 3183f54..7b24cf8 100644 --- a/.github/workflows/validation.yml +++ b/.github/workflows/validation.yml @@ -19,7 +19,7 @@ jobs: run: npm ci --ignore-scripts - name: Run prettier - run: npm run pettier + run: npm run prettier - name: Run linter run: npm run lint From 7ab3ef25210004515842a99e662e3069cc22b261 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 26 Dec 2024 22:55:12 +0100 Subject: [PATCH 052/324] Fix: Dropping licence check (will be back) Fix: added sarif file for anchore --- .github/workflows/anchore.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/anchore.yml b/.github/workflows/anchore.yml index 22632af..57a7fb4 100644 --- a/.github/workflows/anchore.yml +++ b/.github/workflows/anchore.yml @@ -13,7 +13,8 @@ jobs: with: image: "localbuild/testimage:latest" fail-build: false + output-file: ./result.sarif - name: upload Anchore scan SARIF report uses: github/codeql-action/upload-sarif@v3 with: - sarif_file: ${{ steps.scan.outputs.sarif }} + sarif_file: ./result.sarif From d53e0faab41ff40a428b6b097111f9762d33be43 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 26 Dec 2024 22:57:10 +0100 Subject: [PATCH 053/324] Fix: Delete .github/workflows/Licensed.yml Files were deleted on my laptop but not on GH --- .github/workflows/Licensed.yml | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 .github/workflows/Licensed.yml diff --git a/.github/workflows/Licensed.yml b/.github/workflows/Licensed.yml deleted file mode 100644 index 557e7bb..0000000 --- a/.github/workflows/Licensed.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Licensed - -on: [push] - -jobs: - license-check: - runs-on: ubuntu-latest - steps: - - name: Checkout latest code - uses: actions/checkout@4 - - name: Use Node.js 20.x - uses: actions/setup-node - with: - node-version: 20.x - - name: Run npm install - run: npm install - - name: Check licenses - uses: tangro/actions-license-check - with: - allowed-licenses: "MIT; ISC; Apache-2.0; Custom: https://www.telerik.com/kendo-angular-ui/; Custom: https://www.telerik.com/kendo-react-ui/; BSD" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_CONTEXT: ${{ toJson(github) }} From aa38d1b38d9876d56e2dfd0a7a3152b6b4151d43 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 26 Dec 2024 22:57:32 +0100 Subject: [PATCH 054/324] Fix: Delete .github/workflows/license.yml Files were deleted on my laptop but not on GH --- .github/workflows/license.yml | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 .github/workflows/license.yml diff --git a/.github/workflows/license.yml b/.github/workflows/license.yml deleted file mode 100644 index 3089a54..0000000 --- a/.github/workflows/license.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: License checker - -on: [push] - -jobs: - license-check: - runs-on: ubuntu-latest - steps: - - name: Checkout latest code - uses: actions/checkout@v4 - - name: Use Node.js 20.x - uses: actions/setup-node@v4 - with: - node-version: 20.x - - name: Run npm install - run: npm install - - name: Check licenses - uses: tangro/actions-license-check@v1.0.14 - with: - allowed-licenses: "MIT; ISC; Apache-2.0; Custom: https://www.telerik.com/kendo-angular-ui/; Custom: https://www.telerik.com/kendo-react-ui/; BSD" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_CONTEXT: ${{ toJson(github) }} From 66f6c60270ab15e9c0604fe0a649b0c456f6273e Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 26 Dec 2024 22:57:56 +0100 Subject: [PATCH 055/324] Fix: Renamed .github/workflows/test-build.yaml --- .github/workflows/test-build.yaml | 56 ------------------------------- 1 file changed, 56 deletions(-) delete mode 100644 .github/workflows/test-build.yaml diff --git a/.github/workflows/test-build.yaml b/.github/workflows/test-build.yaml deleted file mode 100644 index d298b76..0000000 --- a/.github/workflows/test-build.yaml +++ /dev/null @@ -1,56 +0,0 @@ -name: Test building - -on: [push] - -permissions: - packages: write - contents: read - -jobs: - build-main: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@4 - - # Set up Node.js using nvm - - name: Set up Node.js version from .nvmrc - run: | - curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash - export NVM_DIR="$HOME/.nvm" - [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" - nvm install - nvm use - node -v - npm -v - - - name: Set up QEMU - uses: docker/setup-qemu-action - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action - - - name: Login to Github Container Registry - uses: docker/login-action - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Generate Docker tags - uses: docker/metadata-action - id: metadata - with: - images: ghcr.io/${{ github.repository }} - tags: | - type=raw,enable=true,priority=200,prefix=,suffix=,value=${{ github.sha }} - - - name: Build and Push Docker Images - uses: docker/build-push-action - with: - platforms: linux/amd64,linux/arm64 - push: false - tags: ${{ steps.metadata.outputs.tags }} - labels: ${{ steps.metadata.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max From 93c392c852d1298d55595654396830de059006ed Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 26 Dec 2024 22:58:17 +0100 Subject: [PATCH 056/324] Fix: Delete .github/workflows/licensed.yml Files were deleted on my laptop but not on GH --- .github/workflows/licensed.yml | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 .github/workflows/licensed.yml diff --git a/.github/workflows/licensed.yml b/.github/workflows/licensed.yml deleted file mode 100644 index f384e7a..0000000 --- a/.github/workflows/licensed.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: License checker - -on: [push] - -jobs: - license-check: - runs-on: ubuntu-latest - steps: - - name: Checkout latest code - uses: actions/checkout@4 - - name: Use Node.js 20.x - uses: actions/setup-node@v4 - with: - node-version: 20.x - - name: Run npm install - run: npm install - - name: Check licenses - uses: tangro/actions-license-check@v1 - with: - allowed-licenses: "MIT; ISC; Apache-2.0; Custom: https://www.telerik.com/kendo-angular-ui/; Custom: https://www.telerik.com/kendo-react-ui/; BSD" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_CONTEXT: ${{ toJson(github) }} From 7be77f78556398f10680ea8267ea2a51fc137786 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 26 Dec 2024 23:07:46 +0100 Subject: [PATCH 057/324] Chore: Cleaning up (gn) --- .github/workflows/anchore.yml | 5 ++--- .github/workflows/build-dev.yaml | 2 +- .github/workflows/build-image.yml | 2 +- .github/workflows/build-test.yaml | 2 +- .github/workflows/codeql.yml | 2 +- .github/workflows/remove-stale.yml | 2 +- .github/workflows/validation.yml | 2 +- README.md | 3 ++- TODO.md | 20 ++++++++++---------- package.json | 2 +- 10 files changed, 21 insertions(+), 21 deletions(-) diff --git a/.github/workflows/anchore.yml b/.github/workflows/anchore.yml index 57a7fb4..84eac32 100644 --- a/.github/workflows/anchore.yml +++ b/.github/workflows/anchore.yml @@ -1,6 +1,7 @@ name: Anchore Grype vulnerability scan on: [push] + jobs: build: runs-on: ubuntu-latest @@ -12,9 +13,7 @@ jobs: id: scan with: image: "localbuild/testimage:latest" - fail-build: false - output-file: ./result.sarif - name: upload Anchore scan SARIF report uses: github/codeql-action/upload-sarif@v3 with: - sarif_file: ./result.sarif + sarif_file: ${{ steps.scan.outputs.sarif }} diff --git a/.github/workflows/build-dev.yaml b/.github/workflows/build-dev.yaml index b81287c..f21ab4a 100644 --- a/.github/workflows/build-dev.yaml +++ b/.github/workflows/build-dev.yaml @@ -10,7 +10,7 @@ permissions: contents: read jobs: - build-main: + build-dev: runs-on: ubuntu-latest steps: - name: Set up QEMU diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index d7d131e..17933f9 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -9,7 +9,7 @@ permissions: contents: read jobs: - build-main: + build-release: runs-on: ubuntu-latest steps: - name: Set up QEMU diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index f25efed..2f2322f 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -7,7 +7,7 @@ permissions: contents: read jobs: - build-main: + build-test: runs-on: ubuntu-latest steps: - name: Checkout repository diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index b1af0a7..081205c 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -9,7 +9,7 @@ on: - cron: "32 1 * * 5" jobs: - analyze: + codeql: name: Analyze TypeScript runs-on: "ubuntu-latest" permissions: diff --git a/.github/workflows/remove-stale.yml b/.github/workflows/remove-stale.yml index ccccef9..93d1acd 100644 --- a/.github/workflows/remove-stale.yml +++ b/.github/workflows/remove-stale.yml @@ -4,7 +4,7 @@ on: - cron: "30 1 * * *" jobs: - stale: + remove-stale: runs-on: ubuntu-latest steps: - uses: actions/stale@v9 diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml index 7b24cf8..d46610b 100644 --- a/.github/workflows/validation.yml +++ b/.github/workflows/validation.yml @@ -3,7 +3,7 @@ name: "Run all tests" on: [push] jobs: - build: + validation: runs-on: ubuntu-latest steps: - name: Checkout diff --git a/README.md b/README.md index f602f3b..50246a1 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # DockStatAPI v2 + ![Dockstat Logo](.github/DockStat.png) -*Pipelines:*
+_Pipelines:_
[![Docker Image CI](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-image.yml/badge.svg?branch=main)](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-image.yml)
[![Build dockstatapi:nightly](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-dev.yaml/badge.svg?branch=dev)](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-dev.yaml)
diff --git a/TODO.md b/TODO.md index c4687d7..36d3265 100644 --- a/TODO.md +++ b/TODO.md @@ -1,13 +1,13 @@ -- [X] ~Better Offline mode using "faker" library or self written (probably self written)~ Not needed since there is a docker-compsoe file for local testing integrated inside the repo -- [X] HA compatibility -- [X] !!! Needs testing !!! Add automatic notifications when container state changes, according to selected level for notification service +- [x] ~Better Offline mode using "faker" library or self written (probably self written)~ Not needed since there is a docker-compsoe file for local testing integrated inside the repo +- [x] HA compatibility +- [x] !!! Needs testing !!! Add automatic notifications when container state changes, according to selected level for notification service - [ ] Image update and update notifications - [ ] trigger container restart / stop / start via backend routes -- [X] Add more logging -- [X] Structure code differently -- [X] Write new README and make the docs better -- [X] Update more files to correct TS syntax => remove "any" +- [x] Add more logging +- [x] Structure code differently +- [x] Write new README and make the docs better +- [x] Update more files to correct TS syntax => remove "any" - [ ] Websockets -- [X] Better /api/status endpoint with connection status of each host -- [X] Update notification service -- [X] Adjust process.env variables since they don't really work as expected (See [commit](https://github.com/Its4Nik/dockstatapi/pull/21/commits/a03b58c7a17e269f46216df5492e18d008774961)) +- [x] Better /api/status endpoint with connection status of each host +- [x] Update notification service +- [x] Adjust process.env variables since they don't really work as expected (See [commit](https://github.com/Its4Nik/dockstatapi/pull/21/commits/a03b58c7a17e269f46216df5492e18d008774961)) diff --git a/package.json b/package.json index 20af78a..fa0e693 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "docker:full": "docker compose up -d && [ -z \"$TMUX\" ] && tmux new-session -d -s docker 'docker compose logs -f master' \\; split-window -v 'docker compose logs -f slave' \\; attach-session || echo 'Already inside a tmux session. Exiting.'; docker compose down", "docker:build": "docker build . -t \"dockstatapi:local\" -f ./Dockerfile-dev && docker compose up -d", "docker:build:full": "npm run docker:build && [ -z \"$TMUX\" ] && tmux new-session -d -s docker 'docker compose up -d && docker compose logs -f master' \\; split-window -v 'docker compose logs -f slave' \\; attach-session || echo 'Already inside a tmux session. Exiting.'; docker compose down", - "prettier": "npx prettier -c ./src/**/*.ts --parser typescript --write", + "prettier": "npx prettier -c ./src/**/*.ts --parser typescript --write && npx prettier -c ./.github/workflows/*.{yaml,yml} --parser yaml --write && npx prettier -c ./**/*.md --parser markdown --write && npx prettier -c ./**/*.json --parser json --write", "lint": "npx eslint", "lint:fix": "npx eslint --fix" }, From 7c669ea2d369b523f13079b3b21532e12a5806cc Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 27 Dec 2024 19:12:02 +0100 Subject: [PATCH 058/324] Fix: Test new grype workflow --- .github/workflows/anchore.yml | 10 +- Dockerfile | 4 +- Dockerfile-dev | 4 +- package-lock.json | 771 +++++++++++++++++++--------------- 4 files changed, 452 insertions(+), 337 deletions(-) diff --git a/.github/workflows/anchore.yml b/.github/workflows/anchore.yml index 84eac32..4c8ad74 100644 --- a/.github/workflows/anchore.yml +++ b/.github/workflows/anchore.yml @@ -6,14 +6,14 @@ jobs: build: runs-on: ubuntu-latest steps: + - name: Download Grype + run: curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b $GITHUB_PATH - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Build the Container image run: docker build . --file Dockerfile --tag localbuild/testimage:latest - - uses: anchore/scan-action@v3 - id: scan - with: - image: "localbuild/testimage:latest" + - name: Run Grype test + run: grype -o sarif localbuild/testimage:latest > results.sarif - name: upload Anchore scan SARIF report uses: github/codeql-action/upload-sarif@v3 with: - sarif_file: ${{ steps.scan.outputs.sarif }} + sarif_file: ./results.sarif diff --git a/Dockerfile b/Dockerfile index 26f492b..dc4f58c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,7 +19,7 @@ RUN apk update && \ apk add bash -COPY tsconfig.json environment.d.ts package*.json tsconfig.json yarn.lock ./ +COPY tsconfig.json environment.d.ts package*.json tsconfig.json ./ RUN npm install COPY ./src ./src @@ -38,7 +38,7 @@ WORKDIR /build RUN mkdir -p /build/src/data -COPY tsconfig.json environment.d.ts package*.json tsconfig.json yarn.lock ./ +COPY tsconfig.json environment.d.ts package*.json tsconfig.json ./ RUN npm install --omit=dev COPY --from=builder /build/dist/* /build/src diff --git a/Dockerfile-dev b/Dockerfile-dev index 6e9452a..bd24688 100644 --- a/Dockerfile-dev +++ b/Dockerfile-dev @@ -19,7 +19,7 @@ RUN apk update && \ apk add bash -COPY tsconfig.json environment.d.ts package*.json tsconfig.json yarn.lock ./ +COPY tsconfig.json environment.d.ts package*.json tsconfig.json ./ RUN npm install COPY ./src ./src @@ -38,7 +38,7 @@ WORKDIR /build RUN mkdir -p /build/src/data -COPY tsconfig.json environment.d.ts package*.json tsconfig.json yarn.lock ./ +COPY tsconfig.json environment.d.ts package*.json tsconfig.json ./ RUN npm install --omit=dev COPY --from=builder /build/dist/* /build/src diff --git a/package-lock.json b/package-lock.json index 118aa2b..847bd24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -590,6 +590,30 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@eslint/core": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.1.tgz", @@ -644,6 +668,17 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -657,16 +692,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@eslint/eslintrc/node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -674,17 +699,17 @@ "dev": true, "license": "MIT" }, - "node_modules/@eslint/eslintrc/node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": "*" } }, "node_modules/@eslint/js": { @@ -932,13 +957,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.49.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0.tgz", - "integrity": "sha512-DMulbwQURa8rNIQrf94+jPJQ4FmOVdpE5ZppRNvWVjvhC+6sOeo28r8MgIpQRYouXRtt/FCCXU7zn20jnHR4Qw==", + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.1.tgz", + "integrity": "sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.49.0" + "playwright": "1.49.1" }, "bin": { "playwright": "cli.js" @@ -1117,9 +1142,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.10.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", - "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", + "version": "22.10.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", + "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1195,9 +1220,9 @@ } }, "node_modules/@types/ssh2/node_modules/@types/node": { - "version": "18.19.67", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.67.tgz", - "integrity": "sha512-wI8uHusga+0ZugNp0Ol/3BqQfEcCCNfojtO6Oou9iVNGPTL6QNSdnUdqq85fRgIorLhLMuPIKpsN98QE9Nh+KQ==", + "version": "18.19.68", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.68.tgz", + "integrity": "sha512-QGtpFH1vB99ZmTa63K4/FU8twThj4fuVSBkGddTp7uIL/cuoLWIUSL2RcOaigBhfR+hg5pgGkBnkoOxrTVBMKw==", "dev": true, "license": "MIT", "dependencies": { @@ -1272,16 +1297,6 @@ "typescript": ">=4.8.4 <5.8.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, "node_modules/@typescript-eslint/parser": { "version": "8.18.2", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.18.2.tgz", @@ -1390,32 +1405,6 @@ "typescript": ">=4.8.4 <5.8.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@typescript-eslint/utils": { "version": "8.18.2", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.18.2.tgz", @@ -1627,26 +1616,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/ansi-styles/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/ansi-styles/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -1857,13 +1826,13 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "node_modules/braces": { @@ -1951,35 +1920,33 @@ "node": ">= 10" } }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "node_modules/call-bind-apply-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", + "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", - "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "node_modules/call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", "license": "MIT", "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/call-me-maybe": { @@ -1999,22 +1966,26 @@ } }, "node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" + "node": ">=10" }, "funding": { "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/chokidar": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", - "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "license": "MIT", "dependencies": { "readdirp": "^4.0.1" @@ -2085,18 +2056,22 @@ } }, "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "license": "MIT", "dependencies": { - "color-name": "1.1.3" + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, "node_modules/color-string": { @@ -2118,6 +2093,21 @@ "color-support": "bin.js" } }, + "node_modules/color/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, "node_modules/colorspace": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", @@ -2305,23 +2295,6 @@ "dev": true, "license": "MIT" }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -2348,9 +2321,9 @@ } }, "node_modules/dependency-cruiser": { - "version": "16.7.0", - "resolved": "https://registry.npmjs.org/dependency-cruiser/-/dependency-cruiser-16.7.0.tgz", - "integrity": "sha512-522LLjHINl9r0RIZ8/6s6TqIHTuEJG3XDU2WPSm9dG0rvLUYVyQwE9ID31tDFs4OOyEhdOPaqAaAG1jRv/Zwbg==", + "version": "16.8.0", + "resolved": "https://registry.npmjs.org/dependency-cruiser/-/dependency-cruiser-16.8.0.tgz", + "integrity": "sha512-VyBzIrLHfG7rT36URln+CTy8VSjrLB7YDlMx5vtBSHRHCOXgLUCcP4n5ZoD+s166T0i5LN33q1CvBkEOGsDTSg==", "dev": true, "license": "MIT", "dependencies": { @@ -2375,7 +2348,7 @@ "semver": "^7.6.3", "teamcity-service-messages": "^0.1.14", "tsconfig-paths-webpack-plugin": "^4.2.0", - "watskeburt": "^4.1.1" + "watskeburt": "^4.2.2" }, "bin": { "depcruise": "bin/dependency-cruise.mjs", @@ -2389,6 +2362,16 @@ "node": "^18.17||>=20" } }, + "node_modules/dependency-cruiser/node_modules/ignore": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-6.0.2.tgz", + "integrity": "sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -2460,12 +2443,12 @@ } }, "node_modules/dunder-proto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.0.tgz", - "integrity": "sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.0", + "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" }, @@ -2533,9 +2516,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", - "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz", + "integrity": "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2581,6 +2564,18 @@ "node": ">= 0.4" } }, + "node_modules/es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.23.1", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", @@ -2747,21 +2742,15 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, "node_modules/eslint/node_modules/eslint-visitor-keys": { @@ -2777,39 +2766,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/eslint/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/eslint/node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, "node_modules/eslint/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -2817,17 +2773,17 @@ "dev": true, "license": "MIT" }, - "node_modules/eslint/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "has-flag": "^4.0.0" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=8" + "node": "*" } }, "node_modules/espree": { @@ -2971,9 +2927,9 @@ } }, "node_modules/express-rate-limit": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.4.1.tgz", - "integrity": "sha512-KS3efpnpIDVIXopMc65EMbWbUht7qvTCdtCR2dD/IZmi9MIkopYESwyRqLgv8Pfu589+KqDqOdzJWW7AHoACeg==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", + "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", "license": "MIT", "engines": { "node": ">= 16" @@ -2982,7 +2938,7 @@ "url": "https://github.com/sponsors/express-rate-limit" }, "peerDependencies": { - "express": "4 || 5 || ^5.0.0-beta.1" + "express": "^4.11 || 5 || ^5.0.0-beta.1" } }, "node_modules/express/node_modules/debug": { @@ -3024,6 +2980,19 @@ "node": ">=8.6.0" } }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -3321,19 +3290,21 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.5.tgz", - "integrity": "sha512-Y4+pKa7XeRUPWFNvOOYHkRYrfzW07oraURSvjDmRVOJ748OrVmeXtpE4+GCEHncjCjkTxPNRt8kEbxDhsn6VTg==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz", + "integrity": "sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.0", + "call-bind-apply-helpers": "^1.0.1", "dunder-proto": "^1.0.0", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "gopd": "^1.2.0", "has-symbols": "^1.1.0", - "hasown": "^2.0.2" + "hasown": "^2.0.2", + "math-intrinsics": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -3383,16 +3354,38 @@ } }, "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "license": "ISC", "dependencies": { - "is-glob": "^4.0.1" + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">= 6" + "node": "*" } }, "node_modules/global-directory": { @@ -3451,25 +3444,13 @@ "license": "MIT" }, "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=4" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, "node_modules/has-symbols": { @@ -3602,9 +3583,9 @@ "license": "BSD-3-Clause" }, "node_modules/ignore": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-6.0.2.tgz", - "integrity": "sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", "engines": { @@ -3742,9 +3723,9 @@ } }, "node_modules/is-core-module": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", - "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "dev": true, "license": "MIT", "dependencies": { @@ -4032,6 +4013,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/log-symbols/node_modules/is-unicode-supported": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", @@ -4134,6 +4128,15 @@ "node": ">= 10" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -4273,15 +4276,19 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minimist": { @@ -4584,9 +4591,9 @@ } }, "node_modules/nodemon": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz", - "integrity": "sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz", + "integrity": "sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==", "dev": true, "license": "MIT", "dependencies": { @@ -4612,6 +4619,17 @@ "url": "https://opencollective.com/nodemon" } }, + "node_modules/nodemon/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/nodemon/node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -4637,6 +4655,42 @@ "fsevents": "~2.3.2" } }, + "node_modules/nodemon/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/nodemon/node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -4663,6 +4717,19 @@ "node": ">=8.10.0" } }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", @@ -4830,6 +4897,19 @@ "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, + "node_modules/ora/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/ora/node_modules/emoji-regex": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", @@ -5004,13 +5084,13 @@ } }, "node_modules/playwright": { - "version": "1.49.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0.tgz", - "integrity": "sha512-eKpmys0UFDnfNb3vfsf8Vx2LEOtflgRebl0Im2eQQnYMA4Aqd+Zw8bEOB+7ZKvN76901mRnqdsiOGKxzVTbi7A==", + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.1.tgz", + "integrity": "sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.49.0" + "playwright-core": "1.49.1" }, "bin": { "playwright": "cli.js" @@ -5023,9 +5103,9 @@ } }, "node_modules/playwright-core": { - "version": "1.49.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0.tgz", - "integrity": "sha512-R+3KKTQF3npy5GTiKH/T+kdhoJfJojjHESR1YEWhYuEKRVfVaxH3+4+GvXE5xyCngCxhxnykk0Vlah9v8fs3jA==", + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.1.tgz", + "integrity": "sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==", "dev": true, "license": "Apache-2.0", "bin": { @@ -5252,6 +5332,15 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -5313,19 +5402,22 @@ } }, "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "dev": true, "license": "MIT", "dependencies": { - "is-core-module": "^2.13.0", + "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -5567,23 +5659,6 @@ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", "license": "ISC" }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -5614,15 +5689,69 @@ } }, "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.2", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -5902,25 +6031,29 @@ } }, "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/supports-preserve-symlinks-flag": { @@ -5956,6 +6089,16 @@ "node": ">=12.0.0" } }, + "node_modules/swagger-jsdoc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/swagger-jsdoc/node_modules/commander": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", @@ -5986,6 +6129,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/swagger-jsdoc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/swagger-parser": { "version": "10.0.3", "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz", @@ -6240,46 +6395,6 @@ "node": ">=10.13.0" } }, - "node_modules/tsconfig-paths-webpack-plugin/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/tsconfig-paths-webpack-plugin/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/tsconfig-paths-webpack-plugin/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/tsx": { "version": "4.19.2", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz", From 1cc4073596dce42cd668c2c4928fa5d6dcaa815d Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 27 Dec 2024 19:13:06 +0100 Subject: [PATCH 059/324] Fix: Test new grype workflow --- .github/workflows/anchore.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/anchore.yml b/.github/workflows/anchore.yml index 4c8ad74..00dd53c 100644 --- a/.github/workflows/anchore.yml +++ b/.github/workflows/anchore.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Download Grype - run: curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b $GITHUB_PATH + run: curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b $GITHUB_PATH/grype - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Build the Container image run: docker build . --file Dockerfile --tag localbuild/testimage:latest From a44ac5f3598d307e7442a6b1681a3ecbc4f6f4b8 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 27 Dec 2024 19:14:59 +0100 Subject: [PATCH 060/324] Fix: Test new grype workflow --- .github/workflows/anchore.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/anchore.yml b/.github/workflows/anchore.yml index 00dd53c..02ad165 100644 --- a/.github/workflows/anchore.yml +++ b/.github/workflows/anchore.yml @@ -6,14 +6,17 @@ jobs: build: runs-on: ubuntu-latest steps: + - name: Set up Grype installation path + run: echo "$HOME/bin" >> $GITHUB_PATH - name: Download Grype - run: curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b $GITHUB_PATH/grype - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + run: | + curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b $HOME/bin + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - name: Build the Container image run: docker build . --file Dockerfile --tag localbuild/testimage:latest - name: Run Grype test run: grype -o sarif localbuild/testimage:latest > results.sarif - - name: upload Anchore scan SARIF report + - name: Upload Anchore scan SARIF report uses: github/codeql-action/upload-sarif@v3 with: sarif_file: ./results.sarif From 9b0037899b1ff1f688fade50bdf669ab650daf44 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 27 Dec 2024 19:17:30 +0100 Subject: [PATCH 061/324] Fix: Test new grype workflow --- .github/workflows/anchore.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/anchore.yml b/.github/workflows/anchore.yml index 02ad165..ba2e71a 100644 --- a/.github/workflows/anchore.yml +++ b/.github/workflows/anchore.yml @@ -2,6 +2,10 @@ name: Anchore Grype vulnerability scan on: [push] +permissions: + contents: read + security-events: write + jobs: build: runs-on: ubuntu-latest From 02e14397d3ce5b21a78b1972e03d94a841f3d656 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 27 Dec 2024 19:53:20 +0100 Subject: [PATCH 062/324] Feat: Added credit function (npm run license) --- .github/workflows/anchore.yml | 2 +- CREDITS.md | 52 +++++ package-lock.json | 425 ++++++++++++++++++++++++++++++++++ package.json | 4 +- src/misc/credits.sh | 28 +++ 5 files changed, 509 insertions(+), 2 deletions(-) create mode 100644 CREDITS.md create mode 100644 src/misc/credits.sh diff --git a/.github/workflows/anchore.yml b/.github/workflows/anchore.yml index ba2e71a..2725a7c 100644 --- a/.github/workflows/anchore.yml +++ b/.github/workflows/anchore.yml @@ -7,7 +7,7 @@ permissions: security-events: write jobs: - build: + anchore: runs-on: ubuntu-latest steps: - name: Set up Grype installation path diff --git a/CREDITS.md b/CREDITS.md new file mode 100644 index 0000000..6ff66a2 --- /dev/null +++ b/CREDITS.md @@ -0,0 +1,52 @@ +### License: (MIT AND CC-BY-3.0) + +| Name | Repository | Publisher | +|------|-------------|-----------| +| spdx-ranges@2.1.1 | https://github.com/kemitchell/spdx-ranges.js | The Linux Foundation | + + +### License: Apache-2.0 + +| Name | Repository | Publisher | +|------|-------------|-----------| +| @balena/dockerignore@1.0.2 | https://github.com/balena-io-modules/dockerignore | N/A | +| @eslint/config-array@0.19.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/core@0.9.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/object-schema@2.1.5 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/plugin-kit@0.2.4 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @humanfs/core@0.19.1 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | +| @humanfs/node@0.16.6 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | +| @humanwhocodes/module-importer@1.0.1 | https://github.com/humanwhocodes/module-importer | Nicholas C. Zaks | +| @humanwhocodes/retry@0.3.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | +| @humanwhocodes/retry@0.4.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | +| @playwright/test@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | +| @scarf/scarf@1.4.0 | https://github.com/scarf-sh/scarf-js | Scarf Systems | +| detect-libc@2.0.3 | https://github.com/lovell/detect-libc | Lovell Fuller | +| docker-modem@5.0.3 | https://github.com/apocas/docker-modem | Pedro Dias | +| dockerode@4.0.2 | https://github.com/apocas/dockerode | Pedro Dias | +| doctrine@3.0.0 | https://github.com/eslint/doctrine | N/A | +| eslint-visitor-keys@3.4.3 | https://github.com/eslint/eslint-visitor-keys | Toru Nagashima | +| eslint-visitor-keys@4.2.0 | https://github.com/eslint/js | Toru Nagashima | +| playwright-core@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | +| playwright@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | +| spdx-correct@3.2.0 | https://github.com/jslicense/spdx-correct.js | N/A | +| swagger-ui-dist@5.18.2 | https://github.com/swagger-api/swagger-ui | N/A | +| tunnel-agent@0.6.0 | https://github.com/mikeal/tunnel-agent | Mikeal Rogers | +| typescript@5.7.2 | https://github.com/microsoft/TypeScript | Microsoft Corp. | +| validate-npm-package-license@3.0.4 | https://github.com/kemitchell/validate-npm-package-license.js | Kyle E. Mitchell | + + +### License: CC-BY-3.0 + +| Name | Repository | Publisher | +|------|-------------|-----------| +| spdx-exceptions@2.5.0 | https://github.com/kemitchell/spdx-exceptions.json | The Linux Foundation | + + +### License: Python-2.0 + +| Name | Repository | Publisher | +|------|-------------|-----------| +| argparse@2.0.1 | https://github.com/nodeca/argparse | N/A | + + diff --git a/package-lock.json b/package-lock.json index 847bd24..9776ee4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,6 +43,7 @@ "dependency-cruiser": "^16.5.0", "eslint": "^9.17.0", "globals": "^15.14.0", + "license-checker": "^25.0.1", "nodemon": "^3.1.7", "ora": "^8.1.1", "prettier": "^3.4.2", @@ -1676,12 +1677,29 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, + "node_modules/array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, "node_modules/asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -2264,6 +2282,17 @@ } } }, + "node_modules/debuglog": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz", + "integrity": "sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -2391,6 +2420,17 @@ "node": ">=8" } }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -3483,6 +3523,13 @@ "node": ">= 0.4" } }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true, + "license": "ISC" + }, "node_modules/http-cache-semantics": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", @@ -3888,6 +3935,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -3955,6 +4009,153 @@ "node": ">= 0.8.0" } }, + "node_modules/license-checker": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/license-checker/-/license-checker-25.0.1.tgz", + "integrity": "sha512-mET5AIwl7MR2IAKYYoVBBpV0OnkKQ1xGj2IMMeEFIs42QAkEVjRtFZGWmQ28WeU7MP779iAgOaOy93Mn44mn6g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "chalk": "^2.4.1", + "debug": "^3.1.0", + "mkdirp": "^0.5.1", + "nopt": "^4.0.1", + "read-installed": "~4.0.3", + "semver": "^5.5.0", + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0", + "spdx-satisfies": "^4.0.0", + "treeify": "^1.1.0" + }, + "bin": { + "license-checker": "bin/license-checker" + } + }, + "node_modules/license-checker/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/license-checker/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/license-checker/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/license-checker/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/license-checker/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/license-checker/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/license-checker/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/license-checker/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/license-checker/node_modules/nopt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", + "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "1", + "osenv": "^0.1.4" + }, + "bin": { + "nopt": "bin/nopt.js" + } + }, + "node_modules/license-checker/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/license-checker/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -4745,6 +4946,29 @@ "node": ">=6" } }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -4755,6 +4979,13 @@ "node": ">=0.10.0" } }, + "node_modules/npm-normalize-package-bin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", + "dev": true, + "license": "ISC" + }, "node_modules/npmlog": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", @@ -4951,6 +5182,38 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -5341,6 +5604,49 @@ "node": ">=0.10.0" } }, + "node_modules/read-installed": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/read-installed/-/read-installed-4.0.3.tgz", + "integrity": "sha512-O03wg/IYuV/VtnK2h/KXEt9VIbMUFbk3ERG0Iu4FhLZw0EP0T9znqrYDGn6ncbEsXUFaUjiVAWXHzxwt3lhRPQ==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "debuglog": "^1.0.1", + "read-package-json": "^2.0.0", + "readdir-scoped-modules": "^1.0.0", + "semver": "2 || 3 || 4 || 5", + "slide": "~1.1.3", + "util-extend": "^1.0.1" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.2" + } + }, + "node_modules/read-installed/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/read-package-json": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-2.1.2.tgz", + "integrity": "sha512-D1KmuLQr6ZSJS0tW8hf3WGpRlwszJOXZ3E8Yd/DNRaM5d+1wVRZdHlpGBLAuovjr28LbWvjpWkBHMxpRGGjzNA==", + "deprecated": "This package is no longer supported. Please use @npmcli/package-json instead.", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.1", + "json-parse-even-better-errors": "^2.3.0", + "normalize-package-data": "^2.0.0", + "npm-normalize-package-bin": "^1.0.0" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -5355,6 +5661,20 @@ "node": ">= 6" } }, + "node_modules/readdir-scoped-modules": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz", + "integrity": "sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, + "license": "ISC", + "dependencies": { + "debuglog": "^1.0.1", + "dezalgo": "^1.0.0", + "graceful-fs": "^4.1.2", + "once": "^1.3.0" + } + }, "node_modules/readdirp": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", @@ -5840,6 +6160,16 @@ "dev": true, "license": "MIT" }, + "node_modules/slide": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", + "integrity": "sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "*" + } + }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -5881,6 +6211,73 @@ "node": ">= 10" } }, + "node_modules/spdx-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/spdx-compare/-/spdx-compare-1.0.0.tgz", + "integrity": "sha512-C1mDZOX0hnu0ep9dfmuoi03+eOdDoz2yvK79RxbcrVEG1NO1Ph35yW102DHWKN4pk80nwCgeMmSY5L25VE4D9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-find-index": "^1.0.2", + "spdx-expression-parse": "^3.0.0", + "spdx-ranges": "^2.0.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.20", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz", + "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/spdx-ranges": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/spdx-ranges/-/spdx-ranges-2.1.1.tgz", + "integrity": "sha512-mcdpQFV7UDAgLpXEE/jOMqvK4LBoO0uTQg0uvXUewmEFhpiZx5yJSZITHB8w1ZahKdhfZqP5GPEOKLyEq5p8XA==", + "dev": true, + "license": "(MIT AND CC-BY-3.0)" + }, + "node_modules/spdx-satisfies": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/spdx-satisfies/-/spdx-satisfies-4.0.1.tgz", + "integrity": "sha512-WVzZ/cXAzoNmjCWiEluEA3BjHp5tiUmmhn9MK+X0tBbR9sOqtC6UQwmgCNrAIZvNlMuBUYAaHYfb2oqlF9SwKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-compare": "^1.0.0", + "spdx-expression-parse": "^3.0.0", + "spdx-ranges": "^2.0.0" + } + }, "node_modules/split-ca": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", @@ -6298,6 +6695,16 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "license": "MIT" }, + "node_modules/treeify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/treeify/-/treeify-1.1.0.tgz", + "integrity": "sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/triple-beam": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", @@ -6584,6 +6991,13 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/util-extend": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/util-extend/-/util-extend-1.0.3.tgz", + "integrity": "sha512-mLs5zAK+ctllYBj+iAQvlDCwoxU/WDOUaJkcFudeiAX6OajC6BKXJUa9a+tbtkC11dz2Ufb7h0lyvIOVn4LADA==", + "dev": true, + "license": "MIT" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -6600,6 +7014,17 @@ "dev": true, "license": "MIT" }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, "node_modules/validator": { "version": "13.12.0", "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", diff --git a/package.json b/package.json index fa0e693..466978d 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "docker:build:full": "npm run docker:build && [ -z \"$TMUX\" ] && tmux new-session -d -s docker 'docker compose up -d && docker compose logs -f master' \\; split-window -v 'docker compose logs -f slave' \\; attach-session || echo 'Already inside a tmux session. Exiting.'; docker compose down", "prettier": "npx prettier -c ./src/**/*.ts --parser typescript --write && npx prettier -c ./.github/workflows/*.{yaml,yml} --parser yaml --write && npx prettier -c ./**/*.md --parser markdown --write && npx prettier -c ./**/*.json --parser json --write", "lint": "npx eslint", - "lint:fix": "npx eslint --fix" + "lint:fix": "npx eslint --fix", + "license": "bash ./src/misc/credits.sh" }, "keywords": [], "author": "Its4Nik", @@ -60,6 +61,7 @@ "dependency-cruiser": "^16.5.0", "eslint": "^9.17.0", "globals": "^15.14.0", + "license-checker": "^25.0.1", "nodemon": "^3.1.7", "ora": "^8.1.1", "prettier": "^3.4.2", diff --git a/src/misc/credits.sh b/src/misc/credits.sh new file mode 100644 index 0000000..8d33150 --- /dev/null +++ b/src/misc/credits.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +if ! command -v jq 2>&1 >/dev/null +then + echo "ERROR: jq could not be found" + exit 1 +fi + + +LICENSE_JSON=$(npx license-checker \ + --exclude 'MIT, MIT-0, MIT OR X11, BSD, ISC, Unlicense, CC0-1.0, Python-2.0: 1' \ + --json) +{ + echo -e "# CREDITS\n" + echo "This file shows all npm packages used in DockStatAPI (also Dev packages)" +} + +jq -r ' + to_entries | + group_by(.value.licenses)[] | + "### License: \(.[0].value.licenses)\n\n" + + "| Name | Repository | Publisher |\n|------|-------------|-----------|\n" + + (map( + "| \(.key) | \(.value.repository // "N/A") | \(.value.publisher // "N/A") |" + ) | join("\n")) + "\n\n" +' <<< "$LICENSE_JSON" >> CREDITS.md + +echo "Markdown file with license information has been created: CREDITS.md" From 71a43ee2ae24991593799810a3b349c74d4b45ea Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 27 Dec 2024 19:53:56 +0100 Subject: [PATCH 063/324] Feat: Added credit function (npm run license) --- CREDITS.md | 3 +++ src/misc/credits.sh | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CREDITS.md b/CREDITS.md index 6ff66a2..62f87e6 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -1,3 +1,6 @@ +# CREDITS + +This file shows all npm packages used in DockStatAPI (also Dev packages) ### License: (MIT AND CC-BY-3.0) | Name | Repository | Publisher | diff --git a/src/misc/credits.sh b/src/misc/credits.sh index 8d33150..8a028a7 100644 --- a/src/misc/credits.sh +++ b/src/misc/credits.sh @@ -10,10 +10,11 @@ fi LICENSE_JSON=$(npx license-checker \ --exclude 'MIT, MIT-0, MIT OR X11, BSD, ISC, Unlicense, CC0-1.0, Python-2.0: 1' \ --json) + { echo -e "# CREDITS\n" echo "This file shows all npm packages used in DockStatAPI (also Dev packages)" -} +} > CREDITS.md jq -r ' to_entries | From 2912bc85f03e9a5f688914a2e7fed14afeebb0d3 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 27 Dec 2024 19:54:25 +0100 Subject: [PATCH 064/324] Feat: Added credit function (npm run license) --- src/misc/credits.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/misc/credits.sh b/src/misc/credits.sh index 8a028a7..3db14f6 100644 --- a/src/misc/credits.sh +++ b/src/misc/credits.sh @@ -13,7 +13,7 @@ LICENSE_JSON=$(npx license-checker \ { echo -e "# CREDITS\n" - echo "This file shows all npm packages used in DockStatAPI (also Dev packages)" + echo -e "This file shows all npm packages used in DockStatAPI (also Dev packages)\n" } > CREDITS.md jq -r ' From 887d5df8e8215119ab925e43fcd653a1e4fde845 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 27 Dec 2024 19:54:44 +0100 Subject: [PATCH 065/324] Feat: Git pre-commit hook testing` --- CREDITS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CREDITS.md b/CREDITS.md index 62f87e6..be34b47 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -1,6 +1,7 @@ # CREDITS This file shows all npm packages used in DockStatAPI (also Dev packages) + ### License: (MIT AND CC-BY-3.0) | Name | Repository | Publisher | From b36a03e175d5bb9939a70555a7229ac56883e570 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 27 Dec 2024 21:56:02 +0100 Subject: [PATCH 066/324] Fix: Add linting Chore: Update docs (can't see it here lmao) --- CREDITS.md | 73 ++++++++++++------------- README.md | 15 +++++ package.json | 5 +- src/config/db.ts | 29 ++++------ src/config/initFiles.ts | 1 - src/misc/createEnvDev.sh | 4 ++ src/misc/createEnvFile.sh | 1 + src/routes/frontendController/routes.ts | 3 +- 8 files changed, 70 insertions(+), 61 deletions(-) diff --git a/CREDITS.md b/CREDITS.md index be34b47..050b430 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -4,53 +4,48 @@ This file shows all npm packages used in DockStatAPI (also Dev packages) ### License: (MIT AND CC-BY-3.0) -| Name | Repository | Publisher | -|------|-------------|-----------| +| Name | Repository | Publisher | +| ----------------- | -------------------------------------------- | -------------------- | | spdx-ranges@2.1.1 | https://github.com/kemitchell/spdx-ranges.js | The Linux Foundation | - ### License: Apache-2.0 -| Name | Repository | Publisher | -|------|-------------|-----------| -| @balena/dockerignore@1.0.2 | https://github.com/balena-io-modules/dockerignore | N/A | -| @eslint/config-array@0.19.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/core@0.9.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/object-schema@2.1.5 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/plugin-kit@0.2.4 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @humanfs/core@0.19.1 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | -| @humanfs/node@0.16.6 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | -| @humanwhocodes/module-importer@1.0.1 | https://github.com/humanwhocodes/module-importer | Nicholas C. Zaks | -| @humanwhocodes/retry@0.3.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | -| @humanwhocodes/retry@0.4.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | -| @playwright/test@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | -| @scarf/scarf@1.4.0 | https://github.com/scarf-sh/scarf-js | Scarf Systems | -| detect-libc@2.0.3 | https://github.com/lovell/detect-libc | Lovell Fuller | -| docker-modem@5.0.3 | https://github.com/apocas/docker-modem | Pedro Dias | -| dockerode@4.0.2 | https://github.com/apocas/dockerode | Pedro Dias | -| doctrine@3.0.0 | https://github.com/eslint/doctrine | N/A | -| eslint-visitor-keys@3.4.3 | https://github.com/eslint/eslint-visitor-keys | Toru Nagashima | -| eslint-visitor-keys@4.2.0 | https://github.com/eslint/js | Toru Nagashima | -| playwright-core@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | -| playwright@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | -| spdx-correct@3.2.0 | https://github.com/jslicense/spdx-correct.js | N/A | -| swagger-ui-dist@5.18.2 | https://github.com/swagger-api/swagger-ui | N/A | -| tunnel-agent@0.6.0 | https://github.com/mikeal/tunnel-agent | Mikeal Rogers | -| typescript@5.7.2 | https://github.com/microsoft/TypeScript | Microsoft Corp. | -| validate-npm-package-license@3.0.4 | https://github.com/kemitchell/validate-npm-package-license.js | Kyle E. Mitchell | - +| Name | Repository | Publisher | +| ------------------------------------ | ------------------------------------------------------------- | --------------------- | +| @balena/dockerignore@1.0.2 | https://github.com/balena-io-modules/dockerignore | N/A | +| @eslint/config-array@0.19.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/core@0.9.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/object-schema@2.1.5 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/plugin-kit@0.2.4 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @humanfs/core@0.19.1 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | +| @humanfs/node@0.16.6 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | +| @humanwhocodes/module-importer@1.0.1 | https://github.com/humanwhocodes/module-importer | Nicholas C. Zaks | +| @humanwhocodes/retry@0.3.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | +| @humanwhocodes/retry@0.4.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | +| @playwright/test@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | +| @scarf/scarf@1.4.0 | https://github.com/scarf-sh/scarf-js | Scarf Systems | +| detect-libc@2.0.3 | https://github.com/lovell/detect-libc | Lovell Fuller | +| docker-modem@5.0.3 | https://github.com/apocas/docker-modem | Pedro Dias | +| dockerode@4.0.2 | https://github.com/apocas/dockerode | Pedro Dias | +| doctrine@3.0.0 | https://github.com/eslint/doctrine | N/A | +| eslint-visitor-keys@3.4.3 | https://github.com/eslint/eslint-visitor-keys | Toru Nagashima | +| eslint-visitor-keys@4.2.0 | https://github.com/eslint/js | Toru Nagashima | +| playwright-core@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | +| playwright@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | +| spdx-correct@3.2.0 | https://github.com/jslicense/spdx-correct.js | N/A | +| swagger-ui-dist@5.18.2 | https://github.com/swagger-api/swagger-ui | N/A | +| tunnel-agent@0.6.0 | https://github.com/mikeal/tunnel-agent | Mikeal Rogers | +| typescript@5.7.2 | https://github.com/microsoft/TypeScript | Microsoft Corp. | +| validate-npm-package-license@3.0.4 | https://github.com/kemitchell/validate-npm-package-license.js | Kyle E. Mitchell | ### License: CC-BY-3.0 -| Name | Repository | Publisher | -|------|-------------|-----------| +| Name | Repository | Publisher | +| --------------------- | -------------------------------------------------- | -------------------- | | spdx-exceptions@2.5.0 | https://github.com/kemitchell/spdx-exceptions.json | The Linux Foundation | - ### License: Python-2.0 -| Name | Repository | Publisher | -|------|-------------|-----------| -| argparse@2.0.1 | https://github.com/nodeca/argparse | N/A | - - +| Name | Repository | Publisher | +| -------------- | ---------------------------------- | --------- | +| argparse@2.0.1 | https://github.com/nodeca/argparse | N/A | diff --git a/README.md b/README.md index 50246a1..e24b149 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,21 @@ _⚠️ = Deprecation warning_ - [⚠️ Integrations](https://outline.itsnik.de/s/dockstat/doc/integrations-Agq1oL6HxF) - [⚠️ Backend API reference](https://outline.itsnik.de/s/dockstat/doc/backend-api-reference-YzcBbDvY33) +# Dependencies + +Please see [CREDITS.md](./CREDITS.md). + +To create the credits file use: `npm run license` + +Or if you want it as a pre-commit hook create this file: + +```bash +#!/bin/bash +# .git/hooks/pre-commit + +npm run license +``` + # DockStat(APIs) goals DockStack tries to be a lightweigh and more "dashboard" like then [portainer](https://github.com/portainer/portainer), [cAdvisor](https://github.com/google/cadvisor), [dockge](https://github.com/louislam/dockge), ... diff --git a/package.json b/package.json index 466978d..8190b94 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "dev": "npm run local-env-file && nodemon", "dev:trace": "npm run local-env-file && nodemon --trace-uncaught --trace-warnings", "dep": "bash ./src/utils/createDependencyGraph.sh", - "dep:remove": "bash ./src/utils/removeUnusedDeps.sh && bash ./src/utils/createDependencyGraph.sh", + "dep:remove": "bash ./src/utils/removeUnusedDeps.sh && npm run dep", "build": "npx tsc", "build:mini": "npx tsc && bash ./src/misc/minifyDist.sh --build-only", "mini": "bash ./src/misc/minifyDist.sh", @@ -21,7 +21,8 @@ "prettier": "npx prettier -c ./src/**/*.ts --parser typescript --write && npx prettier -c ./.github/workflows/*.{yaml,yml} --parser yaml --write && npx prettier -c ./**/*.md --parser markdown --write && npx prettier -c ./**/*.json --parser json --write", "lint": "npx eslint", "lint:fix": "npx eslint --fix", - "license": "bash ./src/misc/credits.sh" + "license": "bash ./src/misc/credits.sh", + "finish": "npm run local-env-file && npm run license && npm run prettier && npm run lint" }, "keywords": [], "author": "Its4Nik", diff --git a/src/config/db.ts b/src/config/db.ts index 8086135..6e2c91c 100644 --- a/src/config/db.ts +++ b/src/config/db.ts @@ -3,26 +3,21 @@ import logger from "../utils/logger"; const dbPath: string = "./src/data/database.db"; -const db: sqlite3.Database = new sqlite3.Database( - dbPath, - (err: Error | null) => { - if (err) { - logger.error("Error opening database:", err.message); - } else { - db.run( - `CREATE TABLE IF NOT EXISTS data ( +const db: sqlite3.Database = new sqlite3.Database(dbPath, (error: any) => { + if (error) { + logger.error("Error opening database:", error.message); + } else { + db.run( + `CREATE TABLE IF NOT EXISTS data ( id INTEGER PRIMARY KEY AUTOINCREMENT, info TEXT NOT NULL, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP )`, - () => { - logger.info( - "Database created / opened successfully, table is ready.", - ); - }, - ); - } - }, -); + () => { + logger.info("Database created / opened successfully, table is ready."); + }, + ); + } +}); export default db; diff --git a/src/config/initFiles.ts b/src/config/initFiles.ts index 1f8776a..7982266 100644 --- a/src/config/initFiles.ts +++ b/src/config/initFiles.ts @@ -1,6 +1,5 @@ import { writeFileSync, existsSync } from "fs"; import logger from "../utils/logger"; -import path from "path"; const files = [ { diff --git a/src/misc/createEnvDev.sh b/src/misc/createEnvDev.sh index dde36f6..4a5a0bb 100755 --- a/src/misc/createEnvDev.sh +++ b/src/misc/createEnvDev.sh @@ -1,5 +1,9 @@ +#!/bin/bash + +# Version VERSION="$(cat ./package.json | grep version | cut -d '"' -f 4)" +# Docker if grep -q '/docker' /proc/1/cgroup 2>/dev/null || [ -f /.dockerenv ]; then RUNNING_IN_DOCKER="true" else diff --git a/src/misc/createEnvFile.sh b/src/misc/createEnvFile.sh index d47eaa9..754eab5 100644 --- a/src/misc/createEnvFile.sh +++ b/src/misc/createEnvFile.sh @@ -9,6 +9,7 @@ if grep -q '/docker' /proc/1/cgroup 2>/dev/null || [ -f /.dockerenv ]; then else RUNNING_IN_DOCKER="false" fi + echo -n "\ { \"VERSION\": \"${VERSION}\", diff --git a/src/routes/frontendController/routes.ts b/src/routes/frontendController/routes.ts index fe5d841..540444a 100644 --- a/src/routes/frontendController/routes.ts +++ b/src/routes/frontendController/routes.ts @@ -1,5 +1,4 @@ import express from "express"; -import logger from "../../utils/logger"; const router = express.Router(); import { hideContainer, @@ -69,7 +68,7 @@ router.post("/show/:containerName", async (req, res) => { try { await unhideContainer(containerName); res.status(200).json({ message: "Container unhidden successfully." }); - } catch (error: any) { + } catch (error) { res.status(500).json({ error: error.message }); } }); From c2fd998cb8e28d812259fbf61fabf807ec85bbe9 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 27 Dec 2024 22:01:56 +0100 Subject: [PATCH 067/324] Fix: Fixing dep:remove logic --- CREDITS.md | 73 +++---- package-lock.json | 148 --------------- package.json | 2 - src/misc/dependencyGraphs/mermaid-all.txt | 222 ++++++++++++---------- src/utils/removeUnusedDeps.sh | 36 ++-- 5 files changed, 178 insertions(+), 303 deletions(-) diff --git a/CREDITS.md b/CREDITS.md index 050b430..be34b47 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -4,48 +4,53 @@ This file shows all npm packages used in DockStatAPI (also Dev packages) ### License: (MIT AND CC-BY-3.0) -| Name | Repository | Publisher | -| ----------------- | -------------------------------------------- | -------------------- | +| Name | Repository | Publisher | +|------|-------------|-----------| | spdx-ranges@2.1.1 | https://github.com/kemitchell/spdx-ranges.js | The Linux Foundation | + ### License: Apache-2.0 -| Name | Repository | Publisher | -| ------------------------------------ | ------------------------------------------------------------- | --------------------- | -| @balena/dockerignore@1.0.2 | https://github.com/balena-io-modules/dockerignore | N/A | -| @eslint/config-array@0.19.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/core@0.9.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/object-schema@2.1.5 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/plugin-kit@0.2.4 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @humanfs/core@0.19.1 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | -| @humanfs/node@0.16.6 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | -| @humanwhocodes/module-importer@1.0.1 | https://github.com/humanwhocodes/module-importer | Nicholas C. Zaks | -| @humanwhocodes/retry@0.3.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | -| @humanwhocodes/retry@0.4.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | -| @playwright/test@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | -| @scarf/scarf@1.4.0 | https://github.com/scarf-sh/scarf-js | Scarf Systems | -| detect-libc@2.0.3 | https://github.com/lovell/detect-libc | Lovell Fuller | -| docker-modem@5.0.3 | https://github.com/apocas/docker-modem | Pedro Dias | -| dockerode@4.0.2 | https://github.com/apocas/dockerode | Pedro Dias | -| doctrine@3.0.0 | https://github.com/eslint/doctrine | N/A | -| eslint-visitor-keys@3.4.3 | https://github.com/eslint/eslint-visitor-keys | Toru Nagashima | -| eslint-visitor-keys@4.2.0 | https://github.com/eslint/js | Toru Nagashima | -| playwright-core@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | -| playwright@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | -| spdx-correct@3.2.0 | https://github.com/jslicense/spdx-correct.js | N/A | -| swagger-ui-dist@5.18.2 | https://github.com/swagger-api/swagger-ui | N/A | -| tunnel-agent@0.6.0 | https://github.com/mikeal/tunnel-agent | Mikeal Rogers | -| typescript@5.7.2 | https://github.com/microsoft/TypeScript | Microsoft Corp. | -| validate-npm-package-license@3.0.4 | https://github.com/kemitchell/validate-npm-package-license.js | Kyle E. Mitchell | +| Name | Repository | Publisher | +|------|-------------|-----------| +| @balena/dockerignore@1.0.2 | https://github.com/balena-io-modules/dockerignore | N/A | +| @eslint/config-array@0.19.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/core@0.9.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/object-schema@2.1.5 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/plugin-kit@0.2.4 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @humanfs/core@0.19.1 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | +| @humanfs/node@0.16.6 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | +| @humanwhocodes/module-importer@1.0.1 | https://github.com/humanwhocodes/module-importer | Nicholas C. Zaks | +| @humanwhocodes/retry@0.3.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | +| @humanwhocodes/retry@0.4.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | +| @playwright/test@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | +| @scarf/scarf@1.4.0 | https://github.com/scarf-sh/scarf-js | Scarf Systems | +| detect-libc@2.0.3 | https://github.com/lovell/detect-libc | Lovell Fuller | +| docker-modem@5.0.3 | https://github.com/apocas/docker-modem | Pedro Dias | +| dockerode@4.0.2 | https://github.com/apocas/dockerode | Pedro Dias | +| doctrine@3.0.0 | https://github.com/eslint/doctrine | N/A | +| eslint-visitor-keys@3.4.3 | https://github.com/eslint/eslint-visitor-keys | Toru Nagashima | +| eslint-visitor-keys@4.2.0 | https://github.com/eslint/js | Toru Nagashima | +| playwright-core@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | +| playwright@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | +| spdx-correct@3.2.0 | https://github.com/jslicense/spdx-correct.js | N/A | +| swagger-ui-dist@5.18.2 | https://github.com/swagger-api/swagger-ui | N/A | +| tunnel-agent@0.6.0 | https://github.com/mikeal/tunnel-agent | Mikeal Rogers | +| typescript@5.7.2 | https://github.com/microsoft/TypeScript | Microsoft Corp. | +| validate-npm-package-license@3.0.4 | https://github.com/kemitchell/validate-npm-package-license.js | Kyle E. Mitchell | + ### License: CC-BY-3.0 -| Name | Repository | Publisher | -| --------------------- | -------------------------------------------------- | -------------------- | +| Name | Repository | Publisher | +|------|-------------|-----------| | spdx-exceptions@2.5.0 | https://github.com/kemitchell/spdx-exceptions.json | The Linux Foundation | + ### License: Python-2.0 -| Name | Repository | Publisher | -| -------------- | ---------------------------------- | --------- | -| argparse@2.0.1 | https://github.com/nodeca/argparse | N/A | +| Name | Repository | Publisher | +|------|-------------|-----------| +| argparse@2.0.1 | https://github.com/nodeca/argparse | N/A | + + diff --git a/package-lock.json b/package-lock.json index 9776ee4..ba55c01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,6 @@ "express-rate-limit": "^7.4.1", "https": "^1.0.0", "ipaddr.js": "^2.2.0", - "node-fetch": "^3.3.2", "nodemailer": "^6.9.16", "sqlite3": "^5.1.7", "swagger-jsdoc": "^6.2.8", @@ -33,7 +32,6 @@ "@types/express": "^5.0.0", "@types/express-handlebars": "^5.3.1", "@types/node": "^22.9.0", - "@types/node-fetch": "^2.6.12", "@types/nodemailer": "^6.4.17", "@types/supports-color": "^8.1.3", "@types/swagger-jsdoc": "^6.0.4", @@ -1152,17 +1150,6 @@ "undici-types": "~6.20.0" } }, - "node_modules/@types/node-fetch": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", - "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "form-data": "^4.0.0" - } - }, "node_modules/@types/nodemailer": { "version": "6.4.17", "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz", @@ -1715,13 +1702,6 @@ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "license": "MIT" }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, - "license": "MIT" - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2136,19 +2116,6 @@ "text-hex": "1.0.x" } }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/commander": { "version": "12.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", @@ -2256,15 +2223,6 @@ "node": ">= 8" } }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -2324,16 +2282,6 @@ "dev": true, "license": "MIT" }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -3070,29 +3018,6 @@ "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", "license": "MIT" }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -3202,33 +3127,6 @@ "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", "license": "MIT" }, - "node_modules/form-data": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", - "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "license": "MIT", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -4667,43 +4565,6 @@ "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", "license": "MIT" }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "license": "MIT", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, "node_modules/node-gyp": { "version": "8.4.1", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", @@ -7056,15 +6917,6 @@ "node": "^18||>=20" } }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/package.json b/package.json index 8190b94..9acd952 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,6 @@ "express-rate-limit": "^7.4.1", "https": "^1.0.0", "ipaddr.js": "^2.2.0", - "node-fetch": "^3.3.2", "nodemailer": "^6.9.16", "sqlite3": "^5.1.7", "swagger-jsdoc": "^6.2.8", @@ -52,7 +51,6 @@ "@types/express": "^5.0.0", "@types/express-handlebars": "^5.3.1", "@types/node": "^22.9.0", - "@types/node-fetch": "^2.6.12", "@types/nodemailer": "^6.4.17", "@types/supports-color": "^8.1.3", "@types/swagger-jsdoc": "^6.0.4", diff --git a/src/misc/dependencyGraphs/mermaid-all.txt b/src/misc/dependencyGraphs/mermaid-all.txt index 995f295..d2bae0c 100644 --- a/src/misc/dependencyGraphs/mermaid-all.txt +++ b/src/misc/dependencyGraphs/mermaid-all.txt @@ -3,123 +3,143 @@ flowchart LR 0["server.ts"] subgraph 1["config"] 2["hostsystem.ts"] -B["db.ts"] -1G["swaggerConfig.ts"] +4["variables.ts"] +C["initFiles.ts"] +F["db.ts"] +1L["swaggerConfig.ts"] end 3["os"] -subgraph 4["controllers"] -5["highAvailability.ts"] -9["proxy.ts"] -A["scheduler.ts"] -C["fetchData.ts"] -R["frontendConfiguration.ts"] +subgraph 5["data"] +6["variables.json"] end -6["util"] -7["init.ts"] -8["process"] -subgraph D["utils"] -E["containerService.ts"] -F["dockerClient.ts"] -U["connectionChecker.ts"] -W["extractHostData.ts"] -X["writeOfflineLog.ts"] -subgraph 12["notifications"] -13["_notify.ts"] -14["discord.ts"] -16["_template.ts"] -17["email.ts"] -18["pushbullet.ts"] -19["pushover.ts"] -1A["slack.ts"] -1B["telegram.ts"] -1C["whatsapp.ts"] +subgraph 7["controllers"] +8["highAvailability.ts"] +D["proxy.ts"] +E["scheduler.ts"] +G["fetchData.ts"] +W["frontendConfiguration.ts"] end -1F["swaggerDocs.ts"] +9["util"] +A["init.ts"] +B["process"] +subgraph H["utils"] +I["containerService.ts"] +J["dockerClient.ts"] +M["rateLimitFS.ts"] +Z["connectionChecker.ts"] +11["extractHostData.ts"] +12["writeOfflineLog.ts"] +subgraph 17["notifications"] +18["_notify.ts"] +19["discord.ts"] +1B["_template.ts"] +1C["email.ts"] +1D["pushbullet.ts"] +1E["pushover.ts"] +1F["slack.ts"] +1G["telegram.ts"] +1H["whatsapp.ts"] end -subgraph G["middleware"] -H["authMiddleware.ts"] -I["checkLock.ts"] -J["rateLimiter.ts"] +1K["swaggerDocs.ts"] end -subgraph K["routes"] -subgraph L["auth"] -M["routes.ts"] +subgraph K["middleware"] +L["authMiddleware.ts"] +N["checkLock.ts"] +O["rateLimiter.ts"] end -subgraph N["data"] -O["routes.ts"] +subgraph P["routes"] +subgraph Q["auth"] +R["routes.ts"] end -subgraph P["frontendController"] -Q["routes.ts"] -end -subgraph S["getter"] +subgraph S["data"] T["routes.ts"] end -subgraph Y["highavailability"] -Z["routes.ts"] +subgraph U["frontendController"] +V["routes.ts"] +end +subgraph X["getter"] +Y["routes.ts"] +end +subgraph 13["highavailability"] +14["routes.ts"] end -subgraph 10["notifications"] -11["routes.ts"] +subgraph 15["notifications"] +16["routes.ts"] end -subgraph 1D["setter"] -1E["routes.ts"] +subgraph 1I["setter"] +1J["routes.ts"] end end -V["net"] -15["https"] +10["net"] +1A["https"] 0-->2 -0-->5 -0-->7 +0-->8 +0-->A +2-->4 2-->3 -5-->6 -7-->9 -7-->A -7-->H -7-->I -7-->J -7-->M -7-->O -7-->Q -7-->T -7-->Z -7-->11 -7-->1E -7-->1F -7-->8 -A-->B +4-->6 +8-->4 +8-->9 A-->C -C-->B -C-->E +A-->D +A-->E +A-->L +A-->N +A-->O +A-->R +A-->T +A-->V +A-->Y +A-->14 +A-->16 +A-->1J +A-->1K +A-->B +D-->4 E-->F -O-->B -Q-->R -T-->A -T-->U -T-->E +E-->G +G-->F +G-->I +I-->J +L-->M +N-->M T-->F -T-->W -T-->X -U-->V -Z-->5 -11-->13 -13-->14 -13-->17 -13-->18 -13-->19 -13-->1A -13-->1B -13-->1C -14-->16 -14-->15 -17-->16 -18-->16 -18-->15 -19-->16 -19-->15 -1A-->16 -1A-->15 -1B-->16 -1B-->15 -1C-->16 -1C-->15 -1E-->A -1F-->1G +V-->W +Y-->E +Y-->Z +Y-->I +Y-->J +Y-->11 +Y-->12 +Z-->10 +14-->8 +16-->18 +18-->19 +18-->1C +18-->1D +18-->1E +18-->1F +18-->1G +18-->1H +19-->4 +19-->1B +19-->1A +1C-->4 +1C-->1B +1D-->4 +1D-->1B +1D-->1A +1E-->4 +1E-->1B +1E-->1A +1F-->4 +1F-->1B +1F-->1A +1G-->4 +1G-->1B +1G-->1A +1H-->4 +1H-->1B +1H-->1A +1J-->E +1K-->1L diff --git a/src/utils/removeUnusedDeps.sh b/src/utils/removeUnusedDeps.sh index df72f4b..5e806df 100755 --- a/src/utils/removeUnusedDeps.sh +++ b/src/utils/removeUnusedDeps.sh @@ -2,35 +2,35 @@ echo "Creating unused dependency list" -TMP="$(npx depcheck --ignores @types/node-fetch,uglify-js,@types/supports-color,ipaddr.js,dependency-cruiser,tsx,@types/bcrypt,@types/express,@types/express-handlebars,@types/node,ts-node --quiet --oneline | tail -n 1 | tr -d '\n')" +TMP="$(npx depcheck --ignores https,@typescript-eslint/eslint-plugin,@typescript-eslint/parser,license-checker,uglify-js,@types/supports-color,ipaddr.js,dependency-cruiser,tsx,@types/bcrypt,@types/express,@types/express-handlebars,@types/node,ts-node --quiet --oneline | tail -n 1 | tr -d '\n')" -lines=$(echo "$TMP" | tr -s ' ' '\n' | wc -l) +lines=$(echo -n "$TMP" | tr -s ' ' '\n' | wc -l) if ((lines == 0)); then echo "No unused dependencies." else echo - echo "Removing these unused dependencies:" + echo "Removing these unused dependencies ($lines):" for entry in $TMP; do echo "$entry" done echo -fi -read -n 1 -p "Delete unused dependencies? (y/n) " input -echo + read -n 1 -p "Delete unused dependencies? (y/n) " input + echo -case $input in - Y|y) - COMMAND=$(echo "npm remove $TMP") - $COMMAND - exit 0 - ;; - *) - echo "Aborting" - exit 1 - ;; -esac + case $input in + Y|y) + COMMAND=$(echo "npm remove $TMP") + $COMMAND + exit 0 + ;; + *) + echo "Aborting" + exit 1 + ;; + esac +fi -exit 2 +exit 0 From ba34e961300d705060959062be0b666d4b4ff86d Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 27 Dec 2024 22:10:10 +0100 Subject: [PATCH 068/324] Fix: Fixing some logic things and docs --- src/misc/dependencyGraphs/mermaid-all.txt | 226 ++++++++++------------ src/utils/createDependencyGraph.sh | 5 +- 2 files changed, 107 insertions(+), 124 deletions(-) diff --git a/src/misc/dependencyGraphs/mermaid-all.txt b/src/misc/dependencyGraphs/mermaid-all.txt index d2bae0c..e81fdf8 100644 --- a/src/misc/dependencyGraphs/mermaid-all.txt +++ b/src/misc/dependencyGraphs/mermaid-all.txt @@ -1,145 +1,127 @@ flowchart LR 0["server.ts"] -subgraph 1["config"] -2["hostsystem.ts"] -4["variables.ts"] -C["initFiles.ts"] -F["db.ts"] -1L["swaggerConfig.ts"] +subgraph 1["controllers"] +2["highAvailability.ts"] +A["proxy.ts"] +B["scheduler.ts"] +D["fetchData.ts"] +T["frontendConfiguration.ts"] end -3["os"] -subgraph 5["data"] -6["variables.json"] +3["util"] +subgraph 4["config"] +5["variables.ts"] +9["initFiles.ts"] +C["db.ts"] +1F["swaggerConfig.ts"] end -subgraph 7["controllers"] -8["highAvailability.ts"] -D["proxy.ts"] -E["scheduler.ts"] -G["fetchData.ts"] -W["frontendConfiguration.ts"] +subgraph 6["data"] +7["variables.json"] end -9["util"] -A["init.ts"] -B["process"] -subgraph H["utils"] -I["containerService.ts"] -J["dockerClient.ts"] -M["rateLimitFS.ts"] -Z["connectionChecker.ts"] -11["extractHostData.ts"] -12["writeOfflineLog.ts"] -subgraph 17["notifications"] -18["_notify.ts"] -19["discord.ts"] -1B["_template.ts"] -1C["email.ts"] -1D["pushbullet.ts"] -1E["pushover.ts"] -1F["slack.ts"] -1G["telegram.ts"] -1H["whatsapp.ts"] +8["init.ts"] +subgraph E["utils"] +F["containerService.ts"] +G["dockerClient.ts"] +J["rateLimitFS.ts"] +W["connectionChecker.ts"] +X["writeOfflineLog.ts"] +subgraph 12["notifications"] +13["_notify.ts"] +14["discord.ts"] +15["_template.ts"] +16["email.ts"] +17["pushbullet.ts"] +18["pushover.ts"] +19["slack.ts"] +1A["telegram.ts"] +1B["whatsapp.ts"] end -1K["swaggerDocs.ts"] +1E["swaggerDocs.ts"] end -subgraph K["middleware"] -L["authMiddleware.ts"] -N["checkLock.ts"] -O["rateLimiter.ts"] +subgraph H["middleware"] +I["authMiddleware.ts"] +K["checkLock.ts"] +L["rateLimiter.ts"] end -subgraph P["routes"] -subgraph Q["auth"] -R["routes.ts"] +subgraph M["routes"] +subgraph N["auth"] +O["routes.ts"] end -subgraph S["data"] -T["routes.ts"] +subgraph P["data"] +Q["routes.ts"] end -subgraph U["frontendController"] -V["routes.ts"] +subgraph R["frontendController"] +S["routes.ts"] end -subgraph X["getter"] -Y["routes.ts"] +subgraph U["getter"] +V["routes.ts"] end -subgraph 13["highavailability"] -14["routes.ts"] +subgraph Y["highavailability"] +Z["routes.ts"] end -subgraph 15["notifications"] -16["routes.ts"] +subgraph 10["notifications"] +11["routes.ts"] end -subgraph 1I["setter"] -1J["routes.ts"] +subgraph 1C["setter"] +1D["routes.ts"] end end -10["net"] -1A["https"] 0-->2 0-->8 -0-->A -2-->4 +2-->5 2-->3 -4-->6 -8-->4 +5-->7 8-->9 -A-->C -A-->D -A-->E -A-->L -A-->N -A-->O -A-->R -A-->T -A-->V -A-->Y -A-->14 -A-->16 -A-->1J -A-->1K -A-->B -D-->4 -E-->F -E-->G -G-->F -G-->I +8-->A +8-->B +8-->I +8-->K +8-->L +8-->O +8-->Q +8-->S +8-->V +8-->Z +8-->11 +8-->1D +8-->1E +A-->5 +B-->C +B-->D +D-->C +D-->F +F-->G I-->J -L-->M -N-->M -T-->F +K-->J +Q-->C +S-->T +V-->B V-->W -Y-->E -Y-->Z -Y-->I -Y-->J -Y-->11 -Y-->12 -Z-->10 -14-->8 -16-->18 -18-->19 -18-->1C -18-->1D -18-->1E -18-->1F -18-->1G -18-->1H -19-->4 -19-->1B -19-->1A -1C-->4 -1C-->1B -1D-->4 -1D-->1B -1D-->1A -1E-->4 -1E-->1B -1E-->1A -1F-->4 -1F-->1B -1F-->1A -1G-->4 -1G-->1B -1G-->1A -1H-->4 -1H-->1B -1H-->1A -1J-->E -1K-->1L +V-->F +V-->G +V-->X +Z-->2 +11-->13 +13-->14 +13-->16 +13-->17 +13-->18 +13-->19 +13-->1A +13-->1B +14-->5 +14-->15 +16-->5 +16-->15 +17-->5 +17-->15 +18-->5 +18-->15 +19-->5 +19-->15 +1A-->5 +1A-->15 +1B-->5 +1B-->15 +1D-->B +1E-->1F diff --git a/src/utils/createDependencyGraph.sh b/src/utils/createDependencyGraph.sh index c822999..9c220f7 100755 --- a/src/utils/createDependencyGraph.sh +++ b/src/utils/createDependencyGraph.sh @@ -1,6 +1,7 @@ #!/bin/bash cd src || exit 1 TMP=$(mktemp) +IGNORE="../node_modules|logger|.dependency-cruiser|path|fs|os|https|net|process" cat ./server.ts | grep "./routes" | awk '{print $2,$4}' > $TMP @@ -14,7 +15,7 @@ spawn_worker(){ npx depcruise \ -p cli-feedback \ -T mermaid \ - -x "../node_modules|logger|.dependency-cruiser|path|fs|net" \ + -x "$IGNORE" \ -f ./misc/dependencyGraphs/mermaid-${route}.txt \ ${target_route} || exit 1 } @@ -26,7 +27,7 @@ done < <(cat $TMP) npx depcruise \ -p cli-feedback \ -T mermaid \ - -x "../node_modules|logger|.dependency-cruiser|path|fs" \ + -x "$IGNORE" \ -f ./misc/dependencyGraphs/mermaid-all.txt \ ./server.ts || exit 1 From 9fdb89f6a3769d28f33f6b666f45f6f1aad91732 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 27 Dec 2024 22:18:41 +0100 Subject: [PATCH 069/324] Fix: Fixing build (hopefully) --- src/routes/highavailability/routes.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/routes/highavailability/routes.ts b/src/routes/highavailability/routes.ts index bc4cb79..3fadb02 100644 --- a/src/routes/highavailability/routes.ts +++ b/src/routes/highavailability/routes.ts @@ -3,9 +3,7 @@ import { Router, Request, Response } from "express"; import logger from "../../utils/logger"; import { readConfig, - synchronizeFilesWithNodes, prepareFilesForSync, - HighAvailabilityConfig, ensureFileExists, } from "../../controllers/highAvailability"; @@ -44,7 +42,7 @@ router.get("/config", async (req: Request, res: Response) => { router.post( "/sync", async ( - req: Request<{}, {}, SyncRequestBody>, + req: Request<{}, {}, SyncRequestBody>, // eslint-disable-line res: Response, ): Promise => { try { From 8e0967be7d789ed48c07f058971b6850db2044a8 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 27 Dec 2024 22:24:17 +0100 Subject: [PATCH 070/324] Fix: Fixing build (hopefully) --- src/routes/frontendController/routes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/frontendController/routes.ts b/src/routes/frontendController/routes.ts index 540444a..0fce63e 100644 --- a/src/routes/frontendController/routes.ts +++ b/src/routes/frontendController/routes.ts @@ -68,7 +68,7 @@ router.post("/show/:containerName", async (req, res) => { try { await unhideContainer(containerName); res.status(200).json({ message: "Container unhidden successfully." }); - } catch (error) { + } catch (error: any) { res.status(500).json({ error: error.message }); } }); From ea8693acb9f1b45da1dce5d8eeadce79212b0fd7 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 30 Dec 2024 10:26:06 +0100 Subject: [PATCH 071/324] Fix: Linting (No more any :D) --- src/config/db.ts | 6 +- src/config/loggerConfig.ts | 1 - src/config/swaggerConfig.ts | 2 +- src/controllers/containerController.ts | 14 +- src/controllers/fetchData.ts | 4 +- src/controllers/frontendConfiguration.ts | 111 +- src/controllers/highAvailability.ts | 2 +- src/controllers/notificationController.ts | 2 + src/controllers/scheduler.ts | 6 +- src/data/frontendConfiguration.json | 12 +- src/init.ts | 2 +- src/middleware/authMiddleware.ts | 2 +- src/routes/auth/routes.ts | 33 +- src/routes/data/routes.ts | 46 +- src/routes/frontendController/routes.ts | 40 +- src/routes/getter/routes.ts | 41 +- src/routes/notifications/routes.ts | 6 +- src/routes/setter/routes.ts | 10 +- src/typings/dockerConfig.ts | 10 + src/typings/frontendConfig.ts | 12 + src/typings/states.ts | 10 + src/typings/table.ts | 7 + src/utils/connectionChecker.ts | 4 +- src/utils/containerService.ts | 18 +- src/utils/dockerClient.ts | 2 +- src/utils/notifications/_notify.ts | 5 +- src/utils/notifications/_template.ts | 29 +- src/utils/notifications/email.ts | 4 +- src/utils/swaggerDocs.ts | 4 +- src/utils/writeOfflineLog.ts | 26 - yarn.lock | 2852 --------------------- 31 files changed, 259 insertions(+), 3064 deletions(-) create mode 100644 src/typings/dockerConfig.ts create mode 100644 src/typings/frontendConfig.ts create mode 100644 src/typings/states.ts create mode 100644 src/typings/table.ts delete mode 100644 src/utils/writeOfflineLog.ts delete mode 100644 yarn.lock diff --git a/src/config/db.ts b/src/config/db.ts index 6e2c91c..edfe383 100644 --- a/src/config/db.ts +++ b/src/config/db.ts @@ -3,9 +3,9 @@ import logger from "../utils/logger"; const dbPath: string = "./src/data/database.db"; -const db: sqlite3.Database = new sqlite3.Database(dbPath, (error: any) => { - if (error) { - logger.error("Error opening database:", error.message); +const db: sqlite3.Database = new sqlite3.Database(dbPath, (error: unknown) => { + if (error as Error) { + logger.error("Error opening database:", (error as Error).message); } else { db.run( `CREATE TABLE IF NOT EXISTS data ( diff --git a/src/config/loggerConfig.ts b/src/config/loggerConfig.ts index 45feb5c..5d1a33e 100644 --- a/src/config/loggerConfig.ts +++ b/src/config/loggerConfig.ts @@ -7,7 +7,6 @@ const red = "\x1b[31m"; const green = "\x1b[32m"; const yellow = "\x1b[33m"; const blue = "\x1b[34m"; -const pink = "\x1b[38;5;213m"; // Pink color for sync logs const ignoreExitListenerLogs = format((info) => { if ( diff --git a/src/config/swaggerConfig.ts b/src/config/swaggerConfig.ts index 630805e..cab967f 100644 --- a/src/config/swaggerConfig.ts +++ b/src/config/swaggerConfig.ts @@ -18,7 +18,7 @@ const options: { }; }; security: Array<{ - passwordAuth: any[]; + passwordAuth: unknown[]; }>; }; apis: string[]; diff --git a/src/controllers/containerController.ts b/src/controllers/containerController.ts index 1532681..8d3bef3 100644 --- a/src/controllers/containerController.ts +++ b/src/controllers/containerController.ts @@ -10,12 +10,12 @@ const getContainers = async (req: Request, res: Response): Promise => { const containers = await docker.listContainers(); res.status(200).json(containers); - } catch (error: any) { + } catch (error: unknown) { logger.error( - `Error fetching containers from host: ${host} - ${error.message || "Unknown error"} - Full error: ${JSON.stringify(error, null, 2)}`, + `Error fetching containers from host: ${host} - ${(error as Error).message || "Unknown error"} - Full error: ${JSON.stringify(error, null, 2)}`, ); res.status(500).json({ - error: `Error fetching containers: ${error.message || "Unknown error"}`, + error: `Error fetching containers: ${(error as Error).message || "Unknown error"}`, }); } }; @@ -36,13 +36,15 @@ const getContainerStats = async ( `Successfully fetched stats for container: ${containerID} from host: ${containerHost}`, ); res.status(200).json(stats); - } catch (error: any) { + } catch (error: unknown) { logger.error( - `Error fetching stats for container: ${containerID} from host: ${containerHost} - ${error.message}`, + `Error fetching stats for container: ${containerID} from host: ${containerHost} - ${(error as Error).message}`, ); res .status(500) - .json({ error: `Error fetching container stats: ${error.message}` }); + .json({ + error: `Error fetching container stats: ${(error as Error).message}`, + }); } }; diff --git a/src/controllers/fetchData.ts b/src/controllers/fetchData.ts index 238e826..dfc2487 100644 --- a/src/controllers/fetchData.ts +++ b/src/controllers/fetchData.ts @@ -66,9 +66,9 @@ const fetchData = async (): Promise => { } else { logger.info("No state change detected, notifications not triggered."); } - } catch (error: any) { + } catch (error: unknown) { logger.error( - `Error fetching data: ${JSON.stringify(error)} \nStack trace: ${error.stack}`, + `Error fetching data: ${JSON.stringify(error)} \nStack trace: ${(error as Error).stack}`, ); } }; diff --git a/src/controllers/frontendConfiguration.ts b/src/controllers/frontendConfiguration.ts index 4d31943..e8e035c 100644 --- a/src/controllers/frontendConfiguration.ts +++ b/src/controllers/frontendConfiguration.ts @@ -4,6 +4,7 @@ const dataPath: string = "./src/data/frontendConfiguration.json"; const expression: string = "https?://(www.)?[-a-zA-Z0-9@:%._+~#=]{1,256}.[a-zA-Z0-9()]{1,6}([-a-zA-Z0-9()@:%_+.~#?&//=]*)"; const regex = new RegExp(expression); +import { FrontendConfig } from "../typings/frontendConfig"; /////////////////////////////////////////////////////////////// // Hide Containers: @@ -11,7 +12,7 @@ async function hideContainer(containerName: string) { try { const data = await readData(); const containerIndex = data.findIndex( - (container: any) => container.name === containerName, + (container) => container.name === containerName, ); if (containerIndex !== -1) { @@ -21,9 +22,9 @@ async function hideContainer(containerName: string) { data.push({ name: containerName, hidden: true }); await saveData(data); } - } catch (error: any) { - logger.error(error); - throw new Error(error); + } catch (error: unknown) { + logger.error(error as Error); + throw new Error(error as string); } } @@ -31,7 +32,7 @@ async function unhideContainer(containerName: string) { try { const data = await readData(); const containerIndex = data.findIndex( - (container: any) => container.name === containerName, + (container) => container.name === containerName, ); if (containerIndex !== -1) { @@ -39,9 +40,9 @@ async function unhideContainer(containerName: string) { await saveData(data); cleanupData(); } - } catch (error: any) { - logger.error(error); - throw new Error(error); + } catch (error: unknown) { + logger.error(error as Error); + throw new Error(error as string); } } @@ -51,7 +52,7 @@ async function addTagToContainer(containerName: string, tag: string) { try { const data = await readData(); const containerIndex = data.findIndex( - (container: any) => container.name === containerName, + (container) => container.name === containerName, ); if (containerIndex !== -1) { @@ -64,9 +65,9 @@ async function addTagToContainer(containerName: string, tag: string) { data.push({ name: containerName, tags: [tag] }); await saveData(data); } - } catch (error: any) { - logger.error(error); - throw new Error(error); + } catch (error: unknown) { + logger.error(error as Error); + throw new Error(error as string); } } @@ -74,19 +75,19 @@ async function removeTagFromContainer(containerName: string, tag: string) { try { const data = await readData(); const containerIndex = data.findIndex( - (container: any) => container.name === containerName, + (container) => container.name === containerName, ); if (containerIndex !== -1 && data[containerIndex].tags) { data[containerIndex].tags = data[containerIndex].tags.filter( - (t: any) => t !== tag, + (t) => t !== tag, ); await saveData(data); cleanupData(); } - } catch (error: any) { + } catch (error: unknown) { logger.error(error); - throw new Error(error); + throw new Error(error as string); } } @@ -94,9 +95,9 @@ async function removeTagFromContainer(containerName: string, tag: string) { // Pin containers async function pinContainer(containerName: string) { try { - const data: any = await readData(); - const containerIndex: number = data.findIndex( - (container: any) => container.name === containerName, + const data = await readData(); + const containerIndex = data.findIndex( + (container) => container.name === containerName, ); if (containerIndex !== -1) { @@ -106,9 +107,9 @@ async function pinContainer(containerName: string) { data.push({ name: containerName, pinned: true }); await saveData(data); } - } catch (error: any) { - logger.error(error); - throw new Error(error); + } catch (error: unknown) { + logger.error(error as Error); + throw new Error(error as string); } } @@ -116,7 +117,7 @@ async function unpinContainer(containerName: string) { try { const data = await readData(); const containerIndex = data.findIndex( - (container: any) => container.name === containerName, + (container) => container.name === containerName, ); if (containerIndex !== -1) { @@ -124,9 +125,9 @@ async function unpinContainer(containerName: string) { await saveData(data); cleanupData(); } - } catch (error: any) { - logger.error(error); - throw new Error(error); + } catch (error: unknown) { + logger.error(error as Error); + throw new Error(error as string); } } @@ -135,9 +136,9 @@ async function unpinContainer(containerName: string) { async function setLink(containerName: string, link: string) { if (link.match(regex)) { try { - const data: any = await readData(); - const containerIndex: any = data.findIndex( - (container: any) => container.name === containerName, + const data = await readData(); + const containerIndex = data.findIndex( + (container) => container.name === containerName, ); if (containerIndex !== -1) { @@ -147,9 +148,9 @@ async function setLink(containerName: string, link: string) { data.push({ name: containerName, link: `${link}` }); await saveData(data); } - } catch (error: any) { + } catch (error: unknown) { logger.error(error); - throw new Error(error); + throw new Error(error as string); } } else { logger.error(`Provided link is not valid: ${link}`); @@ -161,7 +162,7 @@ async function removeLink(containerName: string) { try { const data = await readData(); const containerIndex = data.findIndex( - (container: any) => container.name === containerName, + (container) => container.name === containerName, ); if (containerIndex !== -1) { @@ -169,9 +170,9 @@ async function removeLink(containerName: string) { await saveData(data); cleanupData(); } - } catch (error: any) { - logger.error(error); - throw new Error(error); + } catch (error: unknown) { + logger.error(error as Error); + throw new Error(error as string); } } @@ -181,7 +182,7 @@ async function setIcon(containerName: string, icon: string, custom: boolean) { try { const data = await readData(); const containerIndex: number = data.findIndex( - (container: any) => container.name === containerName, + (container) => container.name === containerName, ); if (custom === true) { @@ -199,9 +200,9 @@ async function setIcon(containerName: string, icon: string, custom: boolean) { data.push({ name: containerName, icon: `${icon}` }); await saveData(data); } - } catch (error: any) { - logger.error(error); - throw new Error(error); + } catch (error: unknown) { + logger.error(error as Error); + throw new Error(error as string); } } @@ -209,7 +210,7 @@ async function removeIcon(containerName: string) { try { const data = await readData(); const containerIndex = data.findIndex( - (container: any) => container.name === containerName, + (container) => container.name === containerName, ); if (containerIndex !== -1) { @@ -217,9 +218,9 @@ async function removeIcon(containerName: string) { await saveData(data); cleanupData(); } - } catch (error: any) { - logger.error(error); - throw new Error(error); + } catch (error: unknown) { + logger.error(error as Error); + throw new Error(error as string); } } @@ -227,11 +228,13 @@ async function removeIcon(containerName: string) { // Data specific functionss async function readData() { try { - const data = await fs.promises.readFile(dataPath, "utf-8"); - return JSON.parse(data); - } catch (error: any) { - console.error("readData"); - if (error.code === "ENOENT") { + const data: FrontendConfig = JSON.parse( + await fs.promises.readFile(dataPath, "utf-8"), + ); + return data; + } catch (error: unknown) { + console.error(`Error while reading ${dataPath}: ${error as Error}`); + if (error as Error) { await saveData([]); return []; } else { @@ -240,7 +243,7 @@ async function readData() { } } -async function saveData(data: any) { +async function saveData(data: FrontendConfig) { try { await fs.promises.writeFile( dataPath, @@ -248,15 +251,15 @@ async function saveData(data: any) { "utf-8", ); logger.info("Succesfully wrote to file"); - } catch (error: any) { - logger.error(error); + } catch (error: unknown) { + logger.error(error as Error); } } async function cleanupData() { try { const data = await readData(); - let cleanedData = []; + let cleanedData: FrontendConfig = []; if (data && Array.isArray(data)) { cleanedData = data.filter((container) => { @@ -273,8 +276,8 @@ async function cleanupData() { } await saveData(cleanedData); - } catch (error: any) { - logger.error(error); + } catch (error: unknown) { + logger.error(error as Error); } } diff --git a/src/controllers/highAvailability.ts b/src/controllers/highAvailability.ts index dd16bf6..7bf7dc7 100644 --- a/src/controllers/highAvailability.ts +++ b/src/controllers/highAvailability.ts @@ -102,7 +102,7 @@ async function readConfig(): Promise { fs.readFileSync(haMasterPath, "utf-8"), ); return data; - } catch (error: any) { + } catch (error: unknown) { logger.error(`Error reading HA-Config: ${(error as Error).message}`); return null; } finally { diff --git a/src/controllers/notificationController.ts b/src/controllers/notificationController.ts index ad0b1bc..0ece955 100644 --- a/src/controllers/notificationController.ts +++ b/src/controllers/notificationController.ts @@ -56,3 +56,5 @@ async function sendNotification(containerId: string) { notify("whatsapp", containerId); } } + +export default sendNotification; diff --git a/src/controllers/scheduler.ts b/src/controllers/scheduler.ts index 763b67f..caa1948 100644 --- a/src/controllers/scheduler.ts +++ b/src/controllers/scheduler.ts @@ -11,7 +11,7 @@ const scheduleFetch = () => { try { fetchData(); cleanupOldEntries(); - } catch (error: any) { + } catch (error: unknown) { logger.error(`Error during scheduled fetch: ${error}`); } @@ -81,8 +81,8 @@ const cleanupOldEntries = async () => { try { db.run("DELETE FROM data WHERE timestamp < ?", twentyFourHoursAgo, Error); logger.info("Old entries cleared from the database."); - } catch (Error: any) { - logger.error(`Error clearing old entries: ${Error.message}`); + } catch (Error: unknown) { + logger.error(`Error clearing old entries: ${(Error as Error).message}`); } }; diff --git a/src/data/frontendConfiguration.json b/src/data/frontendConfiguration.json index 4697f96..884e0e2 100644 --- a/src/data/frontendConfiguration.json +++ b/src/data/frontendConfiguration.json @@ -2,7 +2,15 @@ { "name": "test", "tags": [ - "123" - ] + "123", + "123", + "321" + ], + "link": "https://google.com", + "icon": "custom/test.png" + }, + { + "name": "test2", + "pinned": true } ] \ No newline at end of file diff --git a/src/init.ts b/src/init.ts index 119950c..8c75737 100644 --- a/src/init.ts +++ b/src/init.ts @@ -27,7 +27,7 @@ const initializeApp = (app: express.Application): void => { next(), ); - swaggerDocs(app as any); + swaggerDocs(app); trustedProxies(app); scheduleFetch(); diff --git a/src/middleware/authMiddleware.ts b/src/middleware/authMiddleware.ts index 08ffd21..500a7fa 100644 --- a/src/middleware/authMiddleware.ts +++ b/src/middleware/authMiddleware.ts @@ -43,7 +43,7 @@ async function authMiddleware( logger.debug("Authentication succesfull"); next(); - } catch (error: any) { + } catch (error: unknown) { logger.error("Error in authMiddleware:", error); res.status(500).json({ message: "Internal server error" }); } diff --git a/src/routes/auth/routes.ts b/src/routes/auth/routes.ts index 4af1388..f7e0b18 100644 --- a/src/routes/auth/routes.ts +++ b/src/routes/auth/routes.ts @@ -7,11 +7,6 @@ const passwordBool: string = "./src/data/usePassword.txt"; const saltRounds: number = 10; const router: Router = Router(); -let passwordData: { - hash: string; - salt: string; -}; - async function authEnabled(): Promise { let isAuthEnabled: boolean = false; let data: string = ""; @@ -19,8 +14,8 @@ async function authEnabled(): Promise { data = await fs.readFile(passwordBool, "utf8"); isAuthEnabled = data.trim() === "true"; return isAuthEnabled; - } catch (error: any) { - logger.error("Error reading file: ", error); + } catch (error: unknown) { + logger.error("Error reading file: ", error as Error); return isAuthEnabled; } } @@ -30,8 +25,8 @@ async function readPasswordFile() { try { data = await fs.readFile(passwordFile, "utf8"); return data; - } catch (error: any) { - logger.error("Could not read saved password: ", error); + } catch (error: unknown) { + logger.error("Could not read saved password: ", error as Error); return data; } } @@ -42,8 +37,8 @@ async function writePasswordFile(passwordData: string) { setTrue(); logger.debug("Authentication enabled"); return "Authentication enabled"; - } catch (error: any) { - logger.error("Error writing password file:", error); + } catch (error: unknown) { + logger.error("Error writing password file:", error as Error); return error; } } @@ -53,8 +48,8 @@ async function setTrue() { await fs.writeFile(passwordBool, "true", "utf8"); logger.info(`Enabled authentication`); return; - } catch (error: any) { - logger.error("Error writing to the file:", error); + } catch (error: unknown) { + logger.error("Error writing to the file:", error as Error); return; } } @@ -64,8 +59,8 @@ async function setFalse() { await fs.writeFile(passwordBool, "false", "utf8"); logger.info(`Disabled authentication`); return; - } catch (error: any) { - logger.error("Error writing to the file:", error); + } catch (error: unknown) { + logger.error("Error writing to the file:", error as Error); return; } } @@ -118,8 +113,8 @@ router.post("/enable", async (req: Request, res: Response): Promise => { res .status(200) .json({ message: "Password Authentication enabled successfully" }); - } catch (error) { - logger.error(`Error enabling password authentication: ${error}`); + } catch (error: unknown) { + logger.error(`Error enabling password authentication: ${error as Error}`); res.status(500).json({ message: "An error occurred" }); } }); @@ -165,8 +160,8 @@ router.post("/disable", async (req: Request, res: Response): Promise => { await setFalse(); // Assuming this is an async function res.status(200).json({ message: "Authentication disabled" }); - } catch (error) { - logger.error(`Error disabling authentication: ${error}`); + } catch (error: unknown) { + logger.error(`Error disabling authentication: ${error as Error}`); res.status(500).json({ message: "An error occurred" }); } }); diff --git a/src/routes/data/routes.ts b/src/routes/data/routes.ts index 0e9a6e3..108fafe 100644 --- a/src/routes/data/routes.ts +++ b/src/routes/data/routes.ts @@ -2,14 +2,19 @@ import express from "express"; const router = express.Router(); import db from "../../config/db"; import logger from "../../utils/logger"; +import Table from "../../typings/table"; interface DataRow { info: string; } -function formatRows(rows: DataRow[]): Record { +function formatRows(rows: DataRow[]): Record { return rows.reduce( - (acc: Record, row, index: number): Record => { + ( + acc: Record, + row, + index: number, + ): Record => { acc[index] = JSON.parse(row.info); return acc; }, @@ -88,26 +93,31 @@ function formatRows(rows: DataRow[]): Record { router.get("/latest", (req, res) => { db.get( "SELECT info FROM data ORDER BY timestamp DESC LIMIT 1", - (error, row: any) => { + (error: unknown, row: Partial> | undefined) => { if (error) { - logger.error("Error fetching latest data:", error.message); + logger.error("Error fetching latest data:", (error as Error).message); return res.status(500).json({ error: "Internal server error" }); } - if (!row) { + if (!row || !row.info) { logger.warn("No data available for /data/latest"); return res.status(404).json({ error: "No data available" }); } logger.debug("Fetching /data/latest"); - res.json(JSON.parse(row.info)); + try { + res.json(JSON.parse(row.info)); + } catch (error: unknown) { + logger.error("Error parsing data:", (error as Error).message); + res.status(500).json({ error: "Data format error" }); + } }, ); }); /** * @swagger - * /data/time/24h: + * /data/all: * get: * summary: Retrieve container statistics entries from the last 24 hours * tags: [Database queries] @@ -152,17 +162,27 @@ router.get("/latest", (req, res) => { * type: number * example: 3072 */ -router.get("/time/24h", (req, res) => { +router.get("/all", (req, res) => { const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); + db.all( "SELECT info FROM data WHERE timestamp >= ?", [oneDayAgo], - (error, rows: DataRow[]) => { + (error: unknown, rows: Pick[] | undefined) => { if (error) { - logger.error("Error fetching data from last 24 hours:", error.message); + logger.error( + "Error fetching data from last 24 hours:", + (error as Error).message, + ); return res.status(500).json({ error: "Internal server error" }); } + logger.debug("Fetching /data/time/24h"); + if (!rows || rows.length === 0) { + logger.warn("No data available for /data/time/24h"); + return res.status(404).json({ error: "No data available" }); + } + res.json(formatRows(rows)); }, ); @@ -188,9 +208,9 @@ router.get("/time/24h", (req, res) => { * example: "Database cleared successfully." */ router.delete("/clear", (req, res) => { - db.run("DELETE FROM data", (err) => { - if (err) { - logger.error("Error clearing the database:", err.message); + db.run("DELETE FROM data", (error: unknown) => { + if (error) { + logger.error("Error clearing the database:", (error as Error).message); return res.status(500).json({ error: "Internal server error" }); } logger.debug("Database cleared successfully"); diff --git a/src/routes/frontendController/routes.ts b/src/routes/frontendController/routes.ts index 0fce63e..0de95fe 100644 --- a/src/routes/frontendController/routes.ts +++ b/src/routes/frontendController/routes.ts @@ -68,8 +68,8 @@ router.post("/show/:containerName", async (req, res) => { try { await unhideContainer(containerName); res.status(200).json({ message: "Container unhidden successfully." }); - } catch (error: any) { - res.status(500).json({ error: error.message }); + } catch (error: unknown) { + res.status(500).json({ error: (error as Error).message }); } }); @@ -126,8 +126,8 @@ router.post("/tag/:containerName/:tag", async (req, res) => { try { await addTagToContainer(containerName, tag); res.json({ success: true, message: "Tag added successfully." }); - } catch (error: any) { - res.status(500).json({ success: false, error: error.message }); + } catch (error: unknown) { + res.status(500).json({ success: false, error: (error as Error).message }); } }); @@ -178,8 +178,8 @@ router.post("/pin/:containerName", async (req, res) => { try { await pinContainer(containerName); res.json({ success: true, message: "Container pinned successfully." }); - } catch (error: any) { - res.status(500).json({ success: false, error: error.message }); + } catch (error: unknown) { + res.status(500).json({ success: false, error: (error as Error).message }); } }); @@ -236,8 +236,8 @@ router.post("/add-link/:containerName/:link", async (req, res) => { try { await setLink(containerName, link); res.json({ success: true, message: "Link added successfully." }); - } catch (error: any) { - res.status(500).json({ success: false, error: error.message }); + } catch (error: unknown) { + res.status(500).json({ success: false, error: (error as Error).message }); } }); @@ -304,8 +304,8 @@ router.post( await setIcon(containerName, icon, custom); res.json({ success: true, message: "Icon added successfully." }); - } catch (error: any) { - res.status(500).json({ success: false, error: error.message }); + } catch (error: unknown) { + res.status(500).json({ success: false, error: (error as Error).message }); } }, ); @@ -366,8 +366,8 @@ router.delete("/hide/:containerName", async (req, res) => { try { await hideContainer(target); res.json({ success: true, message: `Container, ${target}, hidden.` }); - } catch (error: any) { - res.status(500).json({ success: false, error: error.message }); + } catch (error: unknown) { + res.status(500).json({ success: false, error: (error as Error).message }); } }); @@ -424,8 +424,8 @@ router.delete("/remove-tag/:containerName/:tag", async (req, res) => { try { await removeTagFromContainer(containerName, tag); res.json({ success: true, message: "Tag removed successfully." }); - } catch (error: any) { - res.status(500).json({ success: false, error: error.message }); + } catch (error: unknown) { + res.status(500).json({ success: false, error: (error as Error).message }); } }); @@ -476,8 +476,8 @@ router.delete("/unpin/:containerName", async (req, res) => { try { await unpinContainer(containerName); res.json({ success: true, message: "Container unpinned successfully." }); - } catch (error: any) { - res.status(500).json({ success: false, error: error.message }); + } catch (error: unknown) { + res.status(500).json({ success: false, error: (error as Error).message }); } }); @@ -528,8 +528,8 @@ router.delete("/remove-link/:containerName", async (req, res) => { try { await removeLink(containerName); res.json({ success: true, message: "Link removed successfully." }); - } catch (error: any) { - res.status(500).json({ success: false, error: error.message }); + } catch (error: unknown) { + res.status(500).json({ success: false, error: (error as Error).message }); } }); @@ -580,8 +580,8 @@ router.delete("/remove-icon/:containerName", async (req, res) => { try { await removeIcon(containerName); res.json({ success: true, message: "Icon removed successfully." }); - } catch (error: any) { - res.status(500).json({ success: false, error: error.message }); + } catch (error: unknown) { + res.status(500).json({ success: false, error: (error as Error).message }); } }); diff --git a/src/routes/getter/routes.ts b/src/routes/getter/routes.ts index 8e3c695..d278075 100644 --- a/src/routes/getter/routes.ts +++ b/src/routes/getter/routes.ts @@ -1,6 +1,5 @@ import extractRelevantData from "../../utils/extractHostData"; import { Router, Request, Response } from "express"; -import { writeOfflineLog, readOfflineLog } from "../../utils/writeOfflineLog"; import getDockerClient from "../../utils/dockerClient"; import fetchAllContainers from "../../utils/containerService"; import { getCurrentSchedule } from "../../controllers/scheduler"; @@ -10,6 +9,7 @@ import checkReachability from "../../utils/connectionChecker"; const configPath = "./src/data/dockerConfig.json"; const router = Router(); const userConf = "./src/data/user.conf"; +import { dockerConfig } from "../../typings/dockerConfig"; /** * @swagger @@ -35,17 +35,17 @@ router.get("/hosts", (req: Request, res: Response) => { logger.info(`Fetching config: ${configPath}`); try { const rawData = fs.readFileSync(configPath, "utf-8"); - const config = JSON.parse(rawData); + const config: dockerConfig = JSON.parse(rawData); if (!config.hosts) { throw new Error("No hosts defined in configuration."); } - const hosts = config.hosts.map((host: any) => host.name); + const hosts = config.hosts.map((host) => host.name); logger.debug("Fetching all available Docker hosts"); res.status(200).json({ hosts }); - } catch (error: any) { - logger.error("Error fetching hosts: " + error.message); + } catch (error: unknown) { + logger.error("Error fetching hosts: " + (error as Error).message); res.status(500).json({ error: "Failed to fetch Docker hosts" }); } }); @@ -86,8 +86,8 @@ router.get("/system", (req: Request, res: Response) => { res.status(500).json({ error: `Error received empty ${userConf}` }); } res.status(200).json(config); - } catch (error: any) { - logger.error(`Could not fetch ${userConf}: ${error}`); + } catch (error: unknown) { + logger.error(`Could not fetch ${userConf}: ${error as Error}`); res.status(500).json({ error: `Failed to fetch ${userConf}` }); } }); @@ -142,14 +142,13 @@ router.get("/host/:hostName/stats", async (req: Request, res: Response) => { const version = await docker.version(); const relevantData = extractRelevantData({ hostName, info, version }); - writeOfflineLog(JSON.stringify(relevantData)); res.status(200).json(relevantData); - } catch (error: any) { + } catch (error: unknown) { logger.error( - `Error fetching stats for host: ${hostName} - ${error.message || "Unknown error"}`, + `Error fetching stats for host: ${hostName} - ${(error as Error).message || "Unknown error"}`, ); res.status(500).json({ - error: `Error fetching host stats: ${error.message || "Unknown error"}`, + error: `Error fetching host stats: ${(error as Error).message || "Unknown error"}`, }); } }); @@ -233,8 +232,8 @@ router.get("/containers", async (req: Request, res: Response) => { const allContainerData = await fetchAllContainers(); logger.debug("Fetched /api/containers"); res.status(200).json(allContainerData); - } catch (error: any) { - logger.error(`Error fetching containers: ${error.message}`); + } catch (error: unknown) { + logger.error(`Error fetching containers: ${(error as Error).message}`); res.status(500).json({ error: "Failed to fetch containers" }); } }); @@ -270,8 +269,10 @@ router.get("/config", async (req: Request, res: Response) => { const jsonData = JSON.parse(rawData.toString()); logger.debug("Fetching /api/config"); res.status(200).json(jsonData); - } catch (error: any) { - logger.error("Error loading dockerConfig.json: " + error.message); + } catch (error: unknown) { + logger.error( + "Error loading dockerConfig.json: " + (error as Error).message, + ); res.status(500).json({ error: "Failed to load Docker configuration" }); } }); @@ -334,8 +335,8 @@ router.get("/status", async (req: Request, res: Response) => { try { const jsonData = await checkReachability(); res.status(200).json(jsonData); - } catch (error: any) { - logger.error(`Error while fetching data: ${error}`); + } catch (error: unknown) { + logger.error(`Error while fetching data: ${error as Error}`); } }); @@ -398,8 +399,10 @@ router.get("/frontend-config", (req: Request, res: Response) => { const jsonData = JSON.parse(rawData.toString()); res.status(200).json(jsonData); - } catch (error: any) { - logger.error("Error loading frontendConfiguration.json: " + error.message); + } catch (error: unknown) { + logger.error( + "Error loading frontendConfiguration.json: " + (error as Error).message, + ); res.status(500).json({ error: "Failed to load Frontend configuration" }); } }); diff --git a/src/routes/notifications/routes.ts b/src/routes/notifications/routes.ts index 262d48f..17cf698 100644 --- a/src/routes/notifications/routes.ts +++ b/src/routes/notifications/routes.ts @@ -12,7 +12,7 @@ interface TemplateData { text: string; } -function isTemplateData(data: any): data is TemplateData { +function isTemplateData(data: TemplateData): data is TemplateData { return ( data !== null && typeof data === "object" && typeof data.text === "string" ); @@ -169,8 +169,8 @@ router.post("/test/:type/:containerId", async (req: Request, res: Response) => { try { await notify(type, containerId); res.json({ success: true, message: `Sent test notification to ${type}` }); - } catch (error: any) { - res.json({ success: false, message: `Errored: ${error}` }); + } catch (error: unknown) { + res.json({ success: false, message: `Errored: ${error as Error}` }); } }); diff --git a/src/routes/setter/routes.ts b/src/routes/setter/routes.ts index fcffeef..96915a9 100644 --- a/src/routes/setter/routes.ts +++ b/src/routes/setter/routes.ts @@ -1,9 +1,9 @@ import { setFetchInterval, parseInterval } from "../../controllers/scheduler"; import logger from "../../utils/logger"; -import { Router, Request, Response } from "express"; +import express, { Router, Request, Response } from "express"; import fs from "fs"; -const router = Router(); +const router: Router = express.Router(); const configPath: string = "./src/data/dockerConfig.json"; interface Host { @@ -101,20 +101,20 @@ router.put( * 400: * description: Invalid interval format or out of range. */ -router.put("/scheduler", (req: any, res: any) => { +router.put("/scheduler", (req: Request, res: Response) => { const interval = req.query.interval as string; try { const newInterval = parseInterval(interval); if (newInterval < 5 * 60 * 1000 || newInterval > 6 * 60 * 60 * 1000) { - return res + res .status(400) .json({ error: "Interval must be between 5 minutes and 6 hours." }); } setFetchInterval(newInterval); - res.json({ message: `Fetch interval set to ${interval}.` }); + res.status(200).json({ message: `Fetch interval set to ${interval}.` }); } catch (error: unknown) { const err = error as Error; logger.error("Error setting fetch interval: " + err.message); diff --git a/src/typings/dockerConfig.ts b/src/typings/dockerConfig.ts new file mode 100644 index 0000000..fea0f4e --- /dev/null +++ b/src/typings/dockerConfig.ts @@ -0,0 +1,10 @@ +interface target { + name: string; + url: string; + port: number; +} + +interface dockerConfig { + hosts: target[]; +} +export { dockerConfig, target }; diff --git a/src/typings/frontendConfig.ts b/src/typings/frontendConfig.ts new file mode 100644 index 0000000..6ce1497 --- /dev/null +++ b/src/typings/frontendConfig.ts @@ -0,0 +1,12 @@ +interface Container { + name: string; + hidden?: boolean; + tags?: string[]; + link?: string; + icon?: string; + pinned?: boolean; +} + +type FrontendConfig = Container[]; + +export { FrontendConfig }; diff --git a/src/typings/states.ts b/src/typings/states.ts new file mode 100644 index 0000000..d5eed20 --- /dev/null +++ b/src/typings/states.ts @@ -0,0 +1,10 @@ +interface Container { + name: string; + id: string; + state: string; + hostName: string; +} + +type ContainerStates = Container[]; + +export { ContainerStates, Container }; diff --git a/src/typings/table.ts b/src/typings/table.ts new file mode 100644 index 0000000..4845eba --- /dev/null +++ b/src/typings/table.ts @@ -0,0 +1,7 @@ +type Table = { + id: number; // Primary key, auto-incremented + info: string; // Non-null text field + timestamp: string; // ISO 8601 formatted datetime string +}; + +export default Table; diff --git a/src/utils/connectionChecker.ts b/src/utils/connectionChecker.ts index 289b9b3..92efba3 100644 --- a/src/utils/connectionChecker.ts +++ b/src/utils/connectionChecker.ts @@ -67,8 +67,8 @@ async function checkReachability(): Promise { const parsedData = JSON.parse(data); const hosts: Host[] = parsedData.hosts; return await checkHostStatus(hosts); - } catch (error: any) { - logger.error(`Error reading file: ${error}`); + } catch (error: unknown) { + logger.error(`Error reading file: ${error as Error}`); return undefined; } } diff --git a/src/utils/containerService.ts b/src/utils/containerService.ts index 0cd09e3..841e9c2 100644 --- a/src/utils/containerService.ts +++ b/src/utils/containerService.ts @@ -6,7 +6,7 @@ const configPath = "./src/data/dockerConfig.json"; interface HostConfig { name: string; - [key: string]: any; + [key: string]: string | number; } interface ContainerData { @@ -44,8 +44,8 @@ function loadConfig() { const configData = fs.readFileSync(configPath, "utf-8"); logger.debug("Loaded " + configPath); return JSON.parse(configData); - } catch (error: any) { - logger.error(`Failed to load config: ${error.message}`); + } catch (error: unknown) { + logger.error(`Failed to load config: ${(error as Error).message}`); return null; } } @@ -62,7 +62,7 @@ async function fetchAllContainers(): Promise { for (const hostConfig of config.hosts as HostConfig[]) { const hostName = hostConfig.name; try { - const docker: any = getDockerClient(hostName); + const docker = getDockerClient(hostName); logger.debug(`Now processing: ${hostName}`); const containers: ContainerInfo[] = await docker.listContainers({ all: true, @@ -103,9 +103,9 @@ async function fetchAllContainers(): Promise { current_net_tx: containerStats.networks?.eth0?.tx_bytes || 0, networkMode: containerInfo.HostConfig.NetworkMode || "unknown", }; - } catch (containerError: any) { + } catch (containerError: unknown) { logger.error( - `Error fetching details for container ID: ${container.Id} on host: ${hostName} - ${containerError.message}`, + `Error fetching details for container ID: ${container.Id} on host: ${hostName} - ${(containerError as Error).message}`, ); return { name: container.Names[0].replace("/", ""), @@ -124,12 +124,12 @@ async function fetchAllContainers(): Promise { } }), ); - } catch (error: any) { + } catch (error: unknown) { logger.error( - `Error fetching containers for host: ${hostName} - ${error.message}. Stack: ${error.stack}`, + `Error fetching containers for host: ${hostName} - ${(error as Error).message}. Stack: ${(error as Error).stack}`, ); allContainerData[hostName] = { - error: `Error fetching containers: ${error.message}`, + error: `Error fetching containers: ${(error as Error).message}`, }; } } diff --git a/src/utils/dockerClient.ts b/src/utils/dockerClient.ts index 4cb3f70..dc0f5e9 100644 --- a/src/utils/dockerClient.ts +++ b/src/utils/dockerClient.ts @@ -19,7 +19,7 @@ function loadDockerConfig(): DockerConfig { const rawData = fs.readFileSync(configPath, "utf-8"); logger.debug("Refreshed DockerConfig.json"); return JSON.parse(rawData) as DockerConfig; - } catch (error: any) { + } catch (error: unknown) { logger.error( "Error loading dockerConfig.json: " + (error as Error).message, ); diff --git a/src/utils/notifications/_notify.ts b/src/utils/notifications/_notify.ts index 139a006..49717f9 100644 --- a/src/utils/notifications/_notify.ts +++ b/src/utils/notifications/_notify.ts @@ -43,9 +43,8 @@ async function notify(type: string, containerId: string) { await pushoverNotification(containerId); break; default: - const errorMsg = "Unknown notification type."; - logger.error(errorMsg); - throw new Error(errorMsg); + logger.error("Unknown notification type."); + throw new Error("Unknown notification type."); } } diff --git a/src/utils/notifications/_template.ts b/src/utils/notifications/_template.ts index 551da82..250f095 100644 --- a/src/utils/notifications/_template.ts +++ b/src/utils/notifications/_template.ts @@ -1,5 +1,6 @@ import fs from "fs"; import logger from "../logger"; +import { ContainerStates, Container } from "../../typings/states"; const templatePath: string = "./src/data/template.json"; const containersPath: string = "./src/data/states.json"; @@ -12,8 +13,8 @@ function getTemplate(): Template | null { try { const data = fs.readFileSync(templatePath, "utf8"); return JSON.parse(data); - } catch (error: any) { - logger.error("Failed to load template:", error); + } catch (error: unknown) { + logger.error("Failed to load template:", error as Error); return null; } } @@ -26,8 +27,8 @@ function setTemplate(newTemplate: string): void { "utf8", ); logger.debug("Template updated successfully"); - } catch (error: any) { - logger.error("Failed to update template:", error); + } catch (error: unknown) { + logger.error("Failed to update template:", error as Error); } } @@ -42,9 +43,11 @@ function renderTemplate(containerId: string): string | null { const data = fs.readFileSync(containersPath, "utf8"); const containers = JSON.parse(data); - let containerData: Record | null = null; + let containerData: ContainerStates | null = null; for (const host in containers) { - containerData = containers[host].find((c: any) => c.id === containerId); + containerData = containers[host].find( + (c: Container) => c.id === containerId, + ); if (containerData) { break; } @@ -56,13 +59,13 @@ function renderTemplate(containerId: string): string | null { } // Substitute placeholders in the template with container data - return Object.keys(containerData).reduce( - (text, key) => - text.replace(new RegExp(`{{${key}}}`, "g"), containerData[key]), - template.text, - ); - } catch (error: any) { - logger.error("Failed to load containers:", error); + return Object.keys(containerData).reduce((text, key) => { + const value = containerData[key as keyof ContainerStates]; + // Convert value to a string to avoid errors + return text.replace(new RegExp(`{{${key}}}`, "g"), String(value)); + }, template.text); + } catch (error: unknown) { + logger.error("Failed to load containers:", error as Error); return null; } } diff --git a/src/utils/notifications/email.ts b/src/utils/notifications/email.ts index 57c94ef..4cd41a1 100644 --- a/src/utils/notifications/email.ts +++ b/src/utils/notifications/email.ts @@ -46,7 +46,7 @@ export async function emailNotification(containerId: string) { try { await transporter.sendMail(mailOptions); - } catch (error: any) { - logger.error("Error sending email:", error); + } catch (error: unknown) { + logger.error("Error sending email:", error as Error); } } diff --git a/src/utils/swaggerDocs.ts b/src/utils/swaggerDocs.ts index 9a386dd..540304a 100644 --- a/src/utils/swaggerDocs.ts +++ b/src/utils/swaggerDocs.ts @@ -1,9 +1,9 @@ import swaggerUi from "swagger-ui-express"; import swaggerJsdoc from "swagger-jsdoc"; import swaggerConfig from "../config/swaggerConfig"; -import { Express } from "express"; +import express from "express"; -const swaggerDocs = (app: Express) => { +const swaggerDocs = (app: express.Application) => { const specs = swaggerJsdoc(swaggerConfig); app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(specs)); }; diff --git a/src/utils/writeOfflineLog.ts b/src/utils/writeOfflineLog.ts deleted file mode 100644 index 244f62e..0000000 --- a/src/utils/writeOfflineLog.ts +++ /dev/null @@ -1,26 +0,0 @@ -import fs from "fs"; -import logger from "../utils/logger"; - -const LOG_FILE_PATH = "./logs/hostStats.json"; - -async function writeOfflineLog(message: string) { - try { - if (!fs.existsSync(LOG_FILE_PATH)) { - await fs.promises.writeFile(LOG_FILE_PATH, message); - } - } catch (error: any) { - logger.error("Error writing one time reference log: ", error); - } -} - -async function readOfflineLog() { - try { - const data = await fs.promises.readFile(LOG_FILE_PATH, "utf-8"); - logger.debug("Returning data:", data); - return data; - } catch (error: any) { - logger.error("Error reading offline log:", error); - } -} - -export { writeOfflineLog, readOfflineLog }; diff --git a/yarn.lock b/yarn.lock deleted file mode 100644 index 9c80049..0000000 --- a/yarn.lock +++ /dev/null @@ -1,2852 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@apidevtools/json-schema-ref-parser@^9.0.6": - version "9.1.2" - resolved "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz" - integrity sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg== - dependencies: - "@jsdevtools/ono" "^7.1.3" - "@types/json-schema" "^7.0.6" - call-me-maybe "^1.0.1" - js-yaml "^4.1.0" - -"@apidevtools/openapi-schemas@^2.0.4": - version "2.1.0" - resolved "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz" - integrity sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ== - -"@apidevtools/swagger-methods@^3.0.2": - version "3.0.2" - resolved "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz" - integrity sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg== - -"@apidevtools/swagger-parser@10.0.3": - version "10.0.3" - resolved "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz" - integrity sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g== - dependencies: - "@apidevtools/json-schema-ref-parser" "^9.0.6" - "@apidevtools/openapi-schemas" "^2.0.4" - "@apidevtools/swagger-methods" "^3.0.2" - "@jsdevtools/ono" "^7.1.3" - call-me-maybe "^1.0.1" - z-schema "^5.0.1" - -"@balena/dockerignore@^1.0.2": - version "1.0.2" - resolved "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz" - integrity sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q== - -"@colors/colors@^1.6.0", "@colors/colors@1.6.0": - version "1.6.0" - resolved "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz" - integrity sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA== - -"@cspotcode/source-map-support@^0.8.0": - version "0.8.1" - resolved "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz" - integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== - dependencies: - "@jridgewell/trace-mapping" "0.3.9" - -"@dabh/diagnostics@^2.0.2": - version "2.0.3" - resolved "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz" - integrity sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA== - dependencies: - colorspace "1.1.x" - enabled "2.0.x" - kuler "^2.0.0" - -"@esbuild/linux-x64@0.23.1": - version "0.23.1" - resolved "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz" - integrity sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ== - -"@gar/promisify@^1.0.1": - version "1.1.3" - resolved "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz" - integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== - -"@jridgewell/resolve-uri@^3.0.3": - version "3.1.2" - resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz" - integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== - -"@jridgewell/sourcemap-codec@^1.4.10": - version "1.5.0" - resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz" - integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== - -"@jridgewell/trace-mapping@0.3.9": - version "0.3.9" - resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz" - integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== - dependencies: - "@jridgewell/resolve-uri" "^3.0.3" - "@jridgewell/sourcemap-codec" "^1.4.10" - -"@jsdevtools/ono@^7.1.3": - version "7.1.3" - resolved "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz" - integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg== - -"@mapbox/node-pre-gyp@^1.0.11": - version "1.0.11" - resolved "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz" - integrity sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ== - dependencies: - detect-libc "^2.0.0" - https-proxy-agent "^5.0.0" - make-dir "^3.1.0" - node-fetch "^2.6.7" - nopt "^5.0.0" - npmlog "^5.0.1" - rimraf "^3.0.2" - semver "^7.3.5" - tar "^6.1.11" - -"@npmcli/fs@^1.0.0": - version "1.1.1" - resolved "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz" - integrity sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ== - dependencies: - "@gar/promisify" "^1.0.1" - semver "^7.3.5" - -"@npmcli/move-file@^1.0.1": - version "1.1.2" - resolved "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz" - integrity sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg== - dependencies: - mkdirp "^1.0.4" - rimraf "^3.0.2" - -"@playwright/test@^1.49.0": - version "1.49.0" - resolved "https://registry.npmjs.org/@playwright/test/-/test-1.49.0.tgz" - integrity sha512-DMulbwQURa8rNIQrf94+jPJQ4FmOVdpE5ZppRNvWVjvhC+6sOeo28r8MgIpQRYouXRtt/FCCXU7zn20jnHR4Qw== - dependencies: - playwright "1.49.0" - -"@scarf/scarf@=1.4.0": - version "1.4.0" - resolved "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz" - integrity sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ== - -"@tootallnate/once@1": - version "1.1.2" - resolved "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz" - integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== - -"@tsconfig/node10@^1.0.7": - version "1.0.11" - resolved "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz" - integrity sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw== - -"@tsconfig/node12@^1.0.7": - version "1.0.11" - resolved "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz" - integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== - -"@tsconfig/node14@^1.0.0": - version "1.0.3" - resolved "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz" - integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== - -"@tsconfig/node16@^1.0.2": - version "1.0.4" - resolved "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz" - integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== - -"@types/bcrypt@^5.0.2": - version "5.0.2" - resolved "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz" - integrity sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ== - dependencies: - "@types/node" "*" - -"@types/body-parser@*": - version "1.19.5" - resolved "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz" - integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== - dependencies: - "@types/connect" "*" - "@types/node" "*" - -"@types/connect@*": - version "3.4.38" - resolved "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz" - integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== - dependencies: - "@types/node" "*" - -"@types/cors@^2.8.17": - version "2.8.17" - resolved "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz" - integrity sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA== - dependencies: - "@types/node" "*" - -"@types/docker-modem@*": - version "3.0.6" - resolved "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz" - integrity sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg== - dependencies: - "@types/node" "*" - "@types/ssh2" "*" - -"@types/dockerode@^3.3.31": - version "3.3.32" - resolved "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.32.tgz" - integrity sha512-xxcG0g5AWKtNyh7I7wswLdFvym4Mlqks5ZlKzxEUrGHS0r0PUOfxm2T0mspwu10mHQqu3Ck3MI3V2HqvLWE1fg== - dependencies: - "@types/docker-modem" "*" - "@types/node" "*" - "@types/ssh2" "*" - -"@types/express-handlebars@^5.3.1": - version "5.3.1" - resolved "https://registry.npmjs.org/@types/express-handlebars/-/express-handlebars-5.3.1.tgz" - integrity sha512-DSzaERLO4gHb8AqnrL58jzSDyT0yDdl6HqDc+bGz1Hf0nrG1FK30nHGzv8NBEGR8QV9eUGB/YaE0Qj3NjF7siw== - -"@types/express-serve-static-core@^5.0.0": - version "5.0.2" - resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.2.tgz" - integrity sha512-vluaspfvWEtE4vcSDlKRNer52DvOGrB2xv6diXy6UKyKW0lqZiWHGNApSyxOv+8DE5Z27IzVvE7hNkxg7EXIcg== - dependencies: - "@types/node" "*" - "@types/qs" "*" - "@types/range-parser" "*" - "@types/send" "*" - -"@types/express@*", "@types/express@^5.0.0": - version "5.0.0" - resolved "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz" - integrity sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ== - dependencies: - "@types/body-parser" "*" - "@types/express-serve-static-core" "^5.0.0" - "@types/qs" "*" - "@types/serve-static" "*" - -"@types/http-errors@*": - version "2.0.4" - resolved "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz" - integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== - -"@types/json-schema@^7.0.6": - version "7.0.15" - resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz" - integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== - -"@types/mime@^1": - version "1.3.5" - resolved "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz" - integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== - -"@types/node-fetch@^2.6.12": - version "2.6.12" - resolved "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz" - integrity sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA== - dependencies: - "@types/node" "*" - form-data "^4.0.0" - -"@types/node@*", "@types/node@^22.9.0": - version "22.10.1" - resolved "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz" - integrity sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ== - dependencies: - undici-types "~6.20.0" - -"@types/node@^18.11.18": - version "18.19.67" - resolved "https://registry.npmjs.org/@types/node/-/node-18.19.67.tgz" - integrity sha512-wI8uHusga+0ZugNp0Ol/3BqQfEcCCNfojtO6Oou9iVNGPTL6QNSdnUdqq85fRgIorLhLMuPIKpsN98QE9Nh+KQ== - dependencies: - undici-types "~5.26.4" - -"@types/nodemailer@^6.4.17": - version "6.4.17" - resolved "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz" - integrity sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww== - dependencies: - "@types/node" "*" - -"@types/qs@*": - version "6.9.17" - resolved "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz" - integrity sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ== - -"@types/range-parser@*": - version "1.2.7" - resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz" - integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== - -"@types/send@*": - version "0.17.4" - resolved "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz" - integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== - dependencies: - "@types/mime" "^1" - "@types/node" "*" - -"@types/serve-static@*": - version "1.15.7" - resolved "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz" - integrity sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw== - dependencies: - "@types/http-errors" "*" - "@types/node" "*" - "@types/send" "*" - -"@types/ssh2@*": - version "1.15.1" - resolved "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.1.tgz" - integrity sha512-ZIbEqKAsi5gj35y4P4vkJYly642wIbY6PqoN0xiyQGshKUGXR9WQjF/iF9mXBQ8uBKy3ezfsCkcoHKhd0BzuDA== - dependencies: - "@types/node" "^18.11.18" - -"@types/supports-color@^8.1.3": - version "8.1.3" - resolved "https://registry.npmjs.org/@types/supports-color/-/supports-color-8.1.3.tgz" - integrity sha512-Hy6UMpxhE3j1tLpl27exp1XqHD7n8chAiNPzWfz16LPZoMMoSc4dzLl6w9qijkEb/r5O1ozdu1CWGA2L83ZeZg== - -"@types/swagger-jsdoc@^6.0.4": - version "6.0.4" - resolved "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.4.tgz" - integrity sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ== - -"@types/swagger-ui-express@^4.1.7": - version "4.1.7" - resolved "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.7.tgz" - integrity sha512-ovLM9dNincXkzH4YwyYpll75vhzPBlWx6La89wwvYH7mHjVpf0X0K/vR/aUM7SRxmr5tt9z7E5XJcjQ46q+S3g== - dependencies: - "@types/express" "*" - "@types/serve-static" "*" - -"@types/triple-beam@^1.3.2": - version "1.3.5" - resolved "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz" - integrity sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw== - -abbrev@1: - version "1.1.1" - resolved "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz" - integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== - -accepts@~1.3.8: - version "1.3.8" - resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz" - integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== - dependencies: - mime-types "~2.1.34" - negotiator "0.6.3" - -acorn-jsx-walk@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/acorn-jsx-walk/-/acorn-jsx-walk-2.0.0.tgz" - integrity sha512-uuo6iJj4D4ygkdzd6jPtcxs8vZgDX9YFIkqczGImoypX2fQ4dVImmu3UzA4ynixCIMTrEOWW+95M2HuBaCEOVA== - -acorn-jsx@^5.3.2: - version "5.3.2" - resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" - integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== - -acorn-loose@^8.4.0: - version "8.4.0" - resolved "https://registry.npmjs.org/acorn-loose/-/acorn-loose-8.4.0.tgz" - integrity sha512-M0EUka6rb+QC4l9Z3T0nJEzNOO7JcoJlYMrBlyBCiFSXRyxjLKayd4TbQs2FDRWQU1h9FR7QVNHt+PEaoNL5rQ== - dependencies: - acorn "^8.11.0" - -acorn-walk@^8.1.1, acorn-walk@^8.3.4: - version "8.3.4" - resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz" - integrity sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g== - dependencies: - acorn "^8.11.0" - -"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.11.0, acorn@^8.14.0, acorn@^8.4.1: - version "8.14.0" - resolved "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz" - integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA== - -agent-base@^6.0.2, agent-base@6: - version "6.0.2" - resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz" - integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== - dependencies: - debug "4" - -agentkeepalive@^4.1.3: - version "4.5.0" - resolved "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz" - integrity sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew== - dependencies: - humanize-ms "^1.2.1" - -aggregate-error@^3.0.0: - version "3.1.0" - resolved "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz" - integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== - dependencies: - clean-stack "^2.0.0" - indent-string "^4.0.0" - -ajv@^8.17.1: - version "8.17.1" - resolved "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz" - integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== - dependencies: - fast-deep-equal "^3.1.3" - fast-uri "^3.0.1" - json-schema-traverse "^1.0.0" - require-from-string "^2.0.2" - -ansi-regex@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" - integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== - -ansi-regex@^6.0.1: - version "6.1.0" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz" - integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== - -ansi-styles@^4.1.0: - version "4.3.0" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - -anymatch@~3.1.2: - version "3.1.3" - resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz" - integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - -"aproba@^1.0.3 || ^2.0.0": - version "2.0.0" - resolved "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz" - integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== - -are-we-there-yet@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz" - integrity sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw== - dependencies: - delegates "^1.0.0" - readable-stream "^3.6.0" - -are-we-there-yet@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz" - integrity sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg== - dependencies: - delegates "^1.0.0" - readable-stream "^3.6.0" - -arg@^4.1.0: - version "4.1.3" - resolved "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz" - integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== - -argparse@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" - integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== - -array-flatten@1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz" - integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== - -asn1@^0.2.6: - version "0.2.6" - resolved "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz" - integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== - dependencies: - safer-buffer "~2.1.0" - -async@^3.2.3: - version "3.2.6" - resolved "https://registry.npmjs.org/async/-/async-3.2.6.tgz" - integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== - -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" - integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -base64-js@^1.3.1: - version "1.5.1" - resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" - integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== - -bcrypt-pbkdf@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz" - integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w== - dependencies: - tweetnacl "^0.14.3" - -bcrypt@^5.1.1: - version "5.1.1" - resolved "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz" - integrity sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww== - dependencies: - "@mapbox/node-pre-gyp" "^1.0.11" - node-addon-api "^5.0.0" - -binary-extensions@^2.0.0: - version "2.3.0" - resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz" - integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== - -bindings@^1.5.0: - version "1.5.0" - resolved "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz" - integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== - dependencies: - file-uri-to-path "1.0.0" - -bl@^4.0.3: - version "4.1.0" - resolved "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz" - integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== - dependencies: - buffer "^5.5.0" - inherits "^2.0.4" - readable-stream "^3.4.0" - -body-parser@1.20.3: - version "1.20.3" - resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz" - integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== - 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.13.0" - raw-body "2.5.2" - type-is "~1.6.18" - unpipe "1.0.0" - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -braces@~3.0.2: - version "3.0.3" - resolved "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz" - integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== - dependencies: - fill-range "^7.1.1" - -buffer@^5.5.0: - version "5.7.1" - resolved "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz" - integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.1.13" - -buildcheck@~0.0.6: - version "0.0.6" - resolved "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz" - integrity sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A== - -bytes@3.1.2: - version "3.1.2" - resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz" - integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== - -cacache@^15.2.0: - version "15.3.0" - resolved "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz" - integrity sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ== - dependencies: - "@npmcli/fs" "^1.0.0" - "@npmcli/move-file" "^1.0.1" - chownr "^2.0.0" - fs-minipass "^2.0.0" - glob "^7.1.4" - infer-owner "^1.0.4" - lru-cache "^6.0.0" - minipass "^3.1.1" - minipass-collect "^1.0.2" - minipass-flush "^1.0.5" - minipass-pipeline "^1.2.2" - mkdirp "^1.0.3" - p-map "^4.0.0" - promise-inflight "^1.0.1" - rimraf "^3.0.2" - ssri "^8.0.1" - tar "^6.0.2" - unique-filename "^1.1.1" - -call-bind-apply-helpers@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz" - integrity sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g== - dependencies: - es-errors "^1.3.0" - function-bind "^1.1.2" - -call-bind@^1.0.7: - version "1.0.8" - resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz" - integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== - dependencies: - call-bind-apply-helpers "^1.0.0" - es-define-property "^1.0.0" - get-intrinsic "^1.2.4" - set-function-length "^1.2.2" - -call-me-maybe@^1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz" - integrity sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ== - -chalk@^4.1.0: - version "4.1.2" - resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -chalk@^5.3.0: - version "5.3.0" - resolved "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz" - integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== - -chokidar@^3.5.2: - version "3.6.0" - resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz" - integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== - dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" - -chokidar@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz" - integrity sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA== - dependencies: - readdirp "^4.0.1" - -chownr@^1.1.1: - version "1.1.4" - resolved "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz" - integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== - -chownr@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz" - integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== - -clean-stack@^2.0.0: - version "2.2.0" - resolved "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz" - integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== - -cli-cursor@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz" - integrity sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw== - dependencies: - restore-cursor "^5.0.0" - -cli-spinners@^2.9.2: - version "2.9.2" - resolved "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz" - integrity sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg== - -color-convert@^1.9.3: - version "1.9.3" - resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@^1.0.0, color-name@1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" - integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== - -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -color-string@^1.6.0: - version "1.9.1" - resolved "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz" - integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== - dependencies: - color-name "^1.0.0" - simple-swizzle "^0.2.2" - -color-support@^1.1.2, color-support@^1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz" - integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== - -color@^3.1.3: - version "3.2.1" - resolved "https://registry.npmjs.org/color/-/color-3.2.1.tgz" - integrity sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA== - dependencies: - color-convert "^1.9.3" - color-string "^1.6.0" - -colorspace@1.1.x: - version "1.1.4" - resolved "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz" - integrity sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w== - dependencies: - color "^3.1.3" - text-hex "1.0.x" - -combined-stream@^1.0.8: - version "1.0.8" - resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" - integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== - dependencies: - delayed-stream "~1.0.0" - -commander@^12.1.0: - version "12.1.0" - resolved "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz" - integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA== - -commander@^9.4.1: - version "9.5.0" - resolved "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz" - integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== - -commander@6.2.0: - version "6.2.0" - resolved "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz" - integrity sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q== - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" - integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== - -console-control-strings@^1.0.0, console-control-strings@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz" - integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== - -content-disposition@0.5.4: - version "0.5.4" - resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz" - integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== - dependencies: - safe-buffer "5.2.1" - -content-type@~1.0.4, content-type@~1.0.5: - version "1.0.5" - resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz" - integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== - -cookie-signature@1.0.6: - version "1.0.6" - resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" - integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== - -cookie@0.7.1: - version "0.7.1" - resolved "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz" - integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== - -cors@^2.8.5: - version "2.8.5" - resolved "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz" - integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== - dependencies: - object-assign "^4" - vary "^1" - -cpu-features@~0.0.10: - version "0.0.10" - resolved "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz" - integrity sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA== - dependencies: - buildcheck "~0.0.6" - nan "^2.19.0" - -create-require@^1.1.0: - version "1.1.1" - resolved "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz" - integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== - -data-uri-to-buffer@^4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz" - integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A== - -debug@^4, debug@^4.1.1, debug@^4.3.3, debug@4: - version "4.4.0" - resolved "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz" - integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== - dependencies: - ms "^2.1.3" - -debug@2.6.9: - version "2.6.9" - resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -decompress-response@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz" - integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== - dependencies: - mimic-response "^3.1.0" - -deep-extend@^0.6.0: - version "0.6.0" - resolved "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz" - integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== - -define-data-property@^1.1.4: - version "1.1.4" - resolved "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz" - integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== - dependencies: - es-define-property "^1.0.0" - es-errors "^1.3.0" - gopd "^1.0.1" - -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" - integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== - -delegates@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz" - integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== - -depd@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" - integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== - -dependency-cruiser@^16.5.0: - version "16.7.0" - resolved "https://registry.npmjs.org/dependency-cruiser/-/dependency-cruiser-16.7.0.tgz" - integrity sha512-522LLjHINl9r0RIZ8/6s6TqIHTuEJG3XDU2WPSm9dG0rvLUYVyQwE9ID31tDFs4OOyEhdOPaqAaAG1jRv/Zwbg== - dependencies: - acorn "^8.14.0" - acorn-jsx "^5.3.2" - acorn-jsx-walk "^2.0.0" - acorn-loose "^8.4.0" - acorn-walk "^8.3.4" - ajv "^8.17.1" - commander "^12.1.0" - enhanced-resolve "^5.17.1" - ignore "^6.0.2" - interpret "^3.1.1" - is-installed-globally "^1.0.0" - json5 "^2.2.3" - memoize "^10.0.0" - picocolors "^1.1.1" - picomatch "^4.0.2" - prompts "^2.4.2" - rechoir "^0.8.0" - safe-regex "^2.1.1" - semver "^7.6.3" - teamcity-service-messages "^0.1.14" - tsconfig-paths-webpack-plugin "^4.2.0" - watskeburt "^4.1.1" - -destroy@1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz" - integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== - -detect-libc@^2.0.0: - version "2.0.3" - resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz" - integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== - -diff@^4.0.1: - version "4.0.2" - resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz" - integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== - -docker-modem@^5.0.3: - version "5.0.3" - resolved "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.3.tgz" - integrity sha512-89zhop5YVhcPEt5FpUFGr3cDyceGhq/F9J+ZndQ4KfqNvfbJpPMfgeixFgUj5OjCYAboElqODxY5Z1EBsSa6sg== - dependencies: - debug "^4.1.1" - readable-stream "^3.5.0" - split-ca "^1.0.1" - ssh2 "^1.15.0" - -dockerode@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/dockerode/-/dockerode-4.0.2.tgz" - integrity sha512-9wM1BVpVMFr2Pw3eJNXrYYt6DT9k0xMcsSCjtPvyQ+xa1iPg/Mo3T/gUcwI0B2cczqCeCYRPF8yFYDwtFXT0+w== - dependencies: - "@balena/dockerignore" "^1.0.2" - docker-modem "^5.0.3" - tar-fs "~2.0.1" - -doctrine@3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz" - integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== - dependencies: - esutils "^2.0.2" - -dunder-proto@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.0.tgz" - integrity sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A== - dependencies: - call-bind-apply-helpers "^1.0.0" - es-errors "^1.3.0" - gopd "^1.2.0" - -ee-first@1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" - integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== - -emoji-regex@^10.3.0: - version "10.4.0" - resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz" - integrity sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw== - -emoji-regex@^8.0.0: - version "8.0.0" - resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - -enabled@2.0.x: - version "2.0.0" - resolved "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz" - integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ== - -encodeurl@~1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" - integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== - -encodeurl@~2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz" - integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== - -encoding@^0.1.0, encoding@^0.1.12: - version "0.1.13" - resolved "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz" - integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== - dependencies: - iconv-lite "^0.6.2" - -end-of-stream@^1.1.0, end-of-stream@^1.4.1: - version "1.4.4" - resolved "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz" - integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== - dependencies: - once "^1.4.0" - -enhanced-resolve@^5.17.1, enhanced-resolve@^5.7.0: - version "5.17.1" - resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz" - integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg== - dependencies: - graceful-fs "^4.2.4" - tapable "^2.2.0" - -env-paths@^2.2.0: - version "2.2.1" - resolved "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz" - integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== - -err-code@^2.0.2: - version "2.0.3" - resolved "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz" - integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== - -es-define-property@^1.0.0, es-define-property@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz" - integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== - -es-errors@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz" - integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== - -esbuild@~0.23.0: - version "0.23.1" - resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz" - integrity sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg== - optionalDependencies: - "@esbuild/aix-ppc64" "0.23.1" - "@esbuild/android-arm" "0.23.1" - "@esbuild/android-arm64" "0.23.1" - "@esbuild/android-x64" "0.23.1" - "@esbuild/darwin-arm64" "0.23.1" - "@esbuild/darwin-x64" "0.23.1" - "@esbuild/freebsd-arm64" "0.23.1" - "@esbuild/freebsd-x64" "0.23.1" - "@esbuild/linux-arm" "0.23.1" - "@esbuild/linux-arm64" "0.23.1" - "@esbuild/linux-ia32" "0.23.1" - "@esbuild/linux-loong64" "0.23.1" - "@esbuild/linux-mips64el" "0.23.1" - "@esbuild/linux-ppc64" "0.23.1" - "@esbuild/linux-riscv64" "0.23.1" - "@esbuild/linux-s390x" "0.23.1" - "@esbuild/linux-x64" "0.23.1" - "@esbuild/netbsd-x64" "0.23.1" - "@esbuild/openbsd-arm64" "0.23.1" - "@esbuild/openbsd-x64" "0.23.1" - "@esbuild/sunos-x64" "0.23.1" - "@esbuild/win32-arm64" "0.23.1" - "@esbuild/win32-ia32" "0.23.1" - "@esbuild/win32-x64" "0.23.1" - -escape-html@~1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" - integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== - -esutils@^2.0.2: - version "2.0.3" - resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" - integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== - -etag@~1.8.1: - version "1.8.1" - resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz" - integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== - -expand-template@^2.0.3: - version "2.0.3" - resolved "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz" - integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== - -express-rate-limit@^7.4.1: - version "7.4.1" - resolved "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.4.1.tgz" - integrity sha512-KS3efpnpIDVIXopMc65EMbWbUht7qvTCdtCR2dD/IZmi9MIkopYESwyRqLgv8Pfu589+KqDqOdzJWW7AHoACeg== - -express@^4.21.1, "express@>=4.0.0 || >=5.0.0-beta", "express@4 || 5 || ^5.0.0-beta.1": - version "4.21.2" - resolved "https://registry.npmjs.org/express/-/express-4.21.2.tgz" - integrity sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA== - dependencies: - accepts "~1.3.8" - array-flatten "1.1.1" - body-parser "1.20.3" - content-disposition "0.5.4" - content-type "~1.0.4" - cookie "0.7.1" - cookie-signature "1.0.6" - debug "2.6.9" - depd "2.0.0" - encodeurl "~2.0.0" - escape-html "~1.0.3" - etag "~1.8.1" - finalhandler "1.3.1" - fresh "0.5.2" - http-errors "2.0.0" - merge-descriptors "1.0.3" - methods "~1.1.2" - on-finished "2.4.1" - parseurl "~1.3.3" - path-to-regexp "0.1.12" - proxy-addr "~2.0.7" - qs "6.13.0" - range-parser "~1.2.1" - safe-buffer "5.2.1" - send "0.19.0" - serve-static "1.16.2" - setprototypeof "1.2.0" - statuses "2.0.1" - type-is "~1.6.18" - utils-merge "1.0.1" - vary "~1.1.2" - -fast-deep-equal@^3.1.3: - version "3.1.3" - resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" - integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== - -fast-uri@^3.0.1: - version "3.0.3" - resolved "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz" - integrity sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw== - -fecha@^4.2.0: - version "4.2.3" - resolved "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz" - integrity sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw== - -fetch-blob@^3.1.2, fetch-blob@^3.1.4: - version "3.2.0" - resolved "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz" - integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ== - dependencies: - node-domexception "^1.0.0" - web-streams-polyfill "^3.0.3" - -file-uri-to-path@1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz" - integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== - -fill-range@^7.1.1: - version "7.1.1" - resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz" - integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== - dependencies: - to-regex-range "^5.0.1" - -finalhandler@1.3.1: - version "1.3.1" - resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz" - integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ== - dependencies: - debug "2.6.9" - encodeurl "~2.0.0" - escape-html "~1.0.3" - on-finished "2.4.1" - parseurl "~1.3.3" - statuses "2.0.1" - unpipe "~1.0.0" - -fn.name@1.x.x: - version "1.1.0" - resolved "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz" - integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== - -form-data@^4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz" - integrity sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - mime-types "^2.1.12" - -formdata-polyfill@^4.0.10: - version "4.0.10" - resolved "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz" - integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g== - dependencies: - fetch-blob "^3.1.2" - -forwarded@0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz" - integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== - -fresh@0.5.2: - version "0.5.2" - resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz" - integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== - -fs-constants@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz" - integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== - -fs-minipass@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz" - integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== - dependencies: - minipass "^3.0.0" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" - integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== - -function-bind@^1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" - integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== - -gauge@^3.0.0: - version "3.0.2" - resolved "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz" - integrity sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q== - dependencies: - aproba "^1.0.3 || ^2.0.0" - color-support "^1.1.2" - console-control-strings "^1.0.0" - has-unicode "^2.0.1" - object-assign "^4.1.1" - signal-exit "^3.0.0" - string-width "^4.2.3" - strip-ansi "^6.0.1" - wide-align "^1.1.2" - -gauge@^4.0.3: - version "4.0.4" - resolved "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz" - integrity sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg== - dependencies: - aproba "^1.0.3 || ^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" - -get-east-asian-width@^1.0.0: - version "1.3.0" - resolved "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz" - integrity sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ== - -get-intrinsic@^1.2.4: - version "1.2.5" - resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.5.tgz" - integrity sha512-Y4+pKa7XeRUPWFNvOOYHkRYrfzW07oraURSvjDmRVOJ748OrVmeXtpE4+GCEHncjCjkTxPNRt8kEbxDhsn6VTg== - dependencies: - call-bind-apply-helpers "^1.0.0" - dunder-proto "^1.0.0" - es-define-property "^1.0.1" - es-errors "^1.3.0" - function-bind "^1.1.2" - gopd "^1.2.0" - has-symbols "^1.1.0" - hasown "^2.0.2" - -get-tsconfig@^4.7.5: - version "4.8.1" - resolved "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz" - integrity sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg== - dependencies: - resolve-pkg-maps "^1.0.0" - -github-from-package@0.0.0: - version "0.0.0" - resolved "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz" - integrity sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw== - -glob-parent@~5.1.2: - version "5.1.2" - resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - -glob@^7.1.3, glob@^7.1.4: - version "7.2.3" - resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" - integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.1.1" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@7.1.6: - version "7.1.6" - resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -global-directory@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz" - integrity sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q== - dependencies: - ini "4.1.1" - -gopd@^1.0.1, gopd@^1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz" - integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== - -graceful-fs@^4.2.4, graceful-fs@^4.2.6: - version "4.2.11" - resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" - integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz" - integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -has-property-descriptors@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz" - integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== - dependencies: - es-define-property "^1.0.0" - -has-symbols@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz" - integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== - -has-unicode@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz" - integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== - -hasown@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz" - integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== - dependencies: - function-bind "^1.1.2" - -http-cache-semantics@^4.1.0: - version "4.1.1" - resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz" - integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== - -http-errors@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz" - integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== - dependencies: - depd "2.0.0" - inherits "2.0.4" - setprototypeof "1.2.0" - statuses "2.0.1" - toidentifier "1.0.1" - -http-proxy-agent@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz" - integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== - dependencies: - "@tootallnate/once" "1" - agent-base "6" - debug "4" - -https-proxy-agent@^5.0.0: - version "5.0.1" - resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz" - integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== - dependencies: - agent-base "6" - debug "4" - -https@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/https/-/https-1.0.0.tgz" - integrity sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg== - -humanize-ms@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz" - integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ== - dependencies: - ms "^2.0.0" - -iconv-lite@^0.6.2: - version "0.6.3" - resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" - integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== - dependencies: - safer-buffer ">= 2.1.2 < 3.0.0" - -iconv-lite@0.4.24: - version "0.4.24" - resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - -ieee754@^1.1.13: - version "1.2.1" - resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - -ignore-by-default@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz" - integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== - -ignore@^6.0.2: - version "6.0.2" - resolved "https://registry.npmjs.org/ignore/-/ignore-6.0.2.tgz" - integrity sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A== - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" - integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== - -indent-string@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz" - integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== - -infer-owner@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz" - integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" - integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@^2.0.3, inherits@^2.0.4, inherits@2, inherits@2.0.4: - version "2.0.4" - resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -ini@~1.3.0: - version "1.3.8" - resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz" - integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== - -ini@4.1.1: - version "4.1.1" - resolved "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz" - integrity sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g== - -interpret@^3.1.1: - version "3.1.1" - resolved "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz" - integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ== - -ip-address@^9.0.5: - version "9.0.5" - resolved "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz" - integrity sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g== - dependencies: - jsbn "1.1.0" - sprintf-js "^1.1.3" - -ipaddr.js@^2.2.0: - version "2.2.0" - resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz" - integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA== - -ipaddr.js@1.9.1: - version "1.9.1" - resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz" - integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== - -is-arrayish@^0.3.1: - version "0.3.2" - resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz" - integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== - -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - -is-core-module@^2.13.0: - version "2.15.1" - resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz" - integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== - dependencies: - hasown "^2.0.2" - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" - integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== - -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" - integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== - -is-glob@^4.0.1, is-glob@~4.0.1: - version "4.0.3" - resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" - integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== - dependencies: - is-extglob "^2.1.1" - -is-installed-globally@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-1.0.0.tgz" - integrity sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ== - dependencies: - global-directory "^4.0.1" - is-path-inside "^4.0.0" - -is-interactive@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz" - integrity sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ== - -is-lambda@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz" - integrity sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ== - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-path-inside@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz" - integrity sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA== - -is-stream@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz" - integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== - -is-unicode-supported@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz" - integrity sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ== - -is-unicode-supported@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz" - integrity sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ== - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" - integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== - -js-yaml@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== - dependencies: - argparse "^2.0.1" - -jsbn@1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz" - integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A== - -json-schema-traverse@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz" - integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== - -json5@^2.2.2, json5@^2.2.3: - version "2.2.3" - resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" - integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== - -kleur@^3.0.3: - version "3.0.3" - resolved "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz" - integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== - -kuler@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz" - integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A== - -lodash.get@^4.4.2: - version "4.4.2" - resolved "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz" - integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ== - -lodash.isequal@^4.5.0: - version "4.5.0" - resolved "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz" - integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== - -lodash.mergewith@^4.6.2: - version "4.6.2" - resolved "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz" - integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== - -log-symbols@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz" - integrity sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw== - dependencies: - chalk "^5.3.0" - is-unicode-supported "^1.3.0" - -logform@^2.7.0: - version "2.7.0" - resolved "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz" - integrity sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ== - dependencies: - "@colors/colors" "1.6.0" - "@types/triple-beam" "^1.3.2" - fecha "^4.2.0" - ms "^2.1.1" - safe-stable-stringify "^2.3.1" - triple-beam "^1.3.0" - -lru-cache@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz" - integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== - dependencies: - yallist "^4.0.0" - -make-dir@^3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz" - integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== - dependencies: - semver "^6.0.0" - -make-error@^1.1.1: - version "1.3.6" - resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz" - integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== - -make-fetch-happen@^9.1.0: - version "9.1.0" - resolved "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz" - integrity sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg== - dependencies: - agentkeepalive "^4.1.3" - cacache "^15.2.0" - http-cache-semantics "^4.1.0" - http-proxy-agent "^4.0.1" - https-proxy-agent "^5.0.0" - is-lambda "^1.0.1" - lru-cache "^6.0.0" - minipass "^3.1.3" - minipass-collect "^1.0.2" - minipass-fetch "^1.3.2" - minipass-flush "^1.0.5" - minipass-pipeline "^1.2.4" - negotiator "^0.6.2" - promise-retry "^2.0.1" - socks-proxy-agent "^6.0.0" - ssri "^8.0.0" - -media-typer@0.3.0: - version "0.3.0" - resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" - integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== - -memoize@^10.0.0: - version "10.0.0" - resolved "https://registry.npmjs.org/memoize/-/memoize-10.0.0.tgz" - integrity sha512-H6cBLgsi6vMWOcCpvVCdFFnl3kerEXbrYh9q+lY6VXvQSmM6CkmV08VOwT+WE2tzIEqRPFfAq3fm4v/UIW6mSA== - dependencies: - mimic-function "^5.0.0" - -merge-descriptors@1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz" - integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== - -methods@~1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" - integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== - -mime-db@1.52.0: - version "1.52.0" - resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" - integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== - -mime-types@^2.1.12, mime-types@~2.1.24, mime-types@~2.1.34: - version "2.1.35" - resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== - dependencies: - mime-db "1.52.0" - -mime@1.6.0: - version "1.6.0" - resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz" - integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== - -mimic-function@^5.0.0: - version "5.0.1" - resolved "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz" - integrity sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA== - -mimic-response@^3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz" - integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== - -minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: - version "3.1.2" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - -minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.6: - version "1.2.8" - resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" - integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== - -minipass-collect@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz" - integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA== - dependencies: - minipass "^3.0.0" - -minipass-fetch@^1.3.2: - version "1.4.1" - resolved "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz" - integrity sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw== - dependencies: - minipass "^3.1.0" - minipass-sized "^1.0.3" - minizlib "^2.0.0" - optionalDependencies: - encoding "^0.1.12" - -minipass-flush@^1.0.5: - version "1.0.5" - resolved "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz" - integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw== - dependencies: - minipass "^3.0.0" - -minipass-pipeline@^1.2.2, minipass-pipeline@^1.2.4: - version "1.2.4" - resolved "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz" - integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== - dependencies: - minipass "^3.0.0" - -minipass-sized@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz" - integrity sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g== - dependencies: - minipass "^3.0.0" - -minipass@^3.0.0, minipass@^3.1.0, minipass@^3.1.1, minipass@^3.1.3: - version "3.3.6" - resolved "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz" - integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== - dependencies: - yallist "^4.0.0" - -minipass@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz" - integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== - -minizlib@^2.0.0, minizlib@^2.1.1: - version "2.1.2" - resolved "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz" - integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== - dependencies: - minipass "^3.0.0" - yallist "^4.0.0" - -mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: - version "0.5.3" - resolved "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz" - integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== - -mkdirp@^1.0.3, mkdirp@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz" - integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== - -ms@^2.0.0, ms@^2.1.1, ms@^2.1.3, ms@2.1.3: - version "2.1.3" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" - integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== - -nan@^2.19.0, nan@^2.20.0: - version "2.22.0" - resolved "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz" - integrity sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw== - -napi-build-utils@^1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz" - integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== - -negotiator@^0.6.2, negotiator@0.6.3: - version "0.6.3" - resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz" - integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== - -node-abi@^3.3.0: - version "3.71.0" - resolved "https://registry.npmjs.org/node-abi/-/node-abi-3.71.0.tgz" - integrity sha512-SZ40vRiy/+wRTf21hxkkEjPJZpARzUMVcJoQse2EF8qkUWbbO2z7vd5oA/H6bVH6SZQ5STGcu0KRDS7biNRfxw== - dependencies: - semver "^7.3.5" - -node-addon-api@^5.0.0: - version "5.1.0" - resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz" - integrity sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA== - -node-addon-api@^7.0.0: - version "7.1.1" - resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz" - integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ== - -node-domexception@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz" - integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== - -node-fetch@^2.6.7: - version "2.7.0" - resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz" - integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== - dependencies: - whatwg-url "^5.0.0" - -node-fetch@^3.3.2: - version "3.3.2" - resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz" - integrity sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA== - dependencies: - data-uri-to-buffer "^4.0.0" - fetch-blob "^3.1.4" - formdata-polyfill "^4.0.10" - -node-gyp@8.x: - version "8.4.1" - resolved "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz" - integrity sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w== - dependencies: - env-paths "^2.2.0" - glob "^7.1.4" - graceful-fs "^4.2.6" - make-fetch-happen "^9.1.0" - nopt "^5.0.0" - npmlog "^6.0.0" - rimraf "^3.0.2" - semver "^7.3.5" - tar "^6.1.2" - which "^2.0.2" - -nodemailer@^6.9.16: - version "6.9.16" - resolved "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.16.tgz" - integrity sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ== - -nodemon@^3.1.7: - version "3.1.7" - resolved "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz" - integrity sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ== - dependencies: - chokidar "^3.5.2" - debug "^4" - ignore-by-default "^1.0.1" - minimatch "^3.1.2" - pstree.remy "^1.1.8" - semver "^7.5.3" - simple-update-notifier "^2.0.0" - supports-color "^5.5.0" - touch "^3.1.0" - undefsafe "^2.0.5" - -nopt@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz" - integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== - dependencies: - abbrev "1" - -normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -npmlog@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz" - integrity sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw== - dependencies: - are-we-there-yet "^2.0.0" - console-control-strings "^1.1.0" - gauge "^3.0.0" - set-blocking "^2.0.0" - -npmlog@^6.0.0: - version "6.0.2" - resolved "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz" - integrity sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg== - dependencies: - are-we-there-yet "^3.0.0" - console-control-strings "^1.1.0" - gauge "^4.0.3" - set-blocking "^2.0.0" - -object-assign@^4, object-assign@^4.1.1: - version "4.1.1" - resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" - integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== - -object-inspect@^1.13.1: - version "1.13.3" - resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz" - integrity sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA== - -on-finished@2.4.1: - version "2.4.1" - resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz" - integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== - dependencies: - ee-first "1.1.1" - -once@^1.3.0, once@^1.3.1, once@^1.4.0: - version "1.4.0" - resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" - integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== - dependencies: - wrappy "1" - -one-time@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz" - integrity sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g== - dependencies: - fn.name "1.x.x" - -onetime@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz" - integrity sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ== - dependencies: - mimic-function "^5.0.0" - -openapi-types@>=7: - version "12.1.3" - resolved "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz" - integrity sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw== - -ora@^8.1.1: - version "8.1.1" - resolved "https://registry.npmjs.org/ora/-/ora-8.1.1.tgz" - integrity sha512-YWielGi1XzG1UTvOaCFaNgEnuhZVMSHYkW/FQ7UX8O26PtlpdM84c0f7wLPlkvx2RfiQmnzd61d/MGxmpQeJPw== - dependencies: - chalk "^5.3.0" - cli-cursor "^5.0.0" - cli-spinners "^2.9.2" - is-interactive "^2.0.0" - is-unicode-supported "^2.0.0" - log-symbols "^6.0.0" - stdin-discarder "^0.2.2" - string-width "^7.2.0" - strip-ansi "^7.1.0" - -p-map@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz" - integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== - dependencies: - aggregate-error "^3.0.0" - -parseurl@~1.3.3: - version "1.3.3" - resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz" - integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" - integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== - -path-parse@^1.0.7: - version "1.0.7" - resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" - integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== - -path-to-regexp@0.1.12: - version "0.1.12" - resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz" - integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ== - -picocolors@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz" - integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== - -picomatch@^2.0.4: - version "2.3.1" - resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== - -picomatch@^2.2.1: - version "2.3.1" - resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== - -picomatch@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz" - integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg== - -playwright-core@1.49.0: - version "1.49.0" - resolved "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0.tgz" - integrity sha512-R+3KKTQF3npy5GTiKH/T+kdhoJfJojjHESR1YEWhYuEKRVfVaxH3+4+GvXE5xyCngCxhxnykk0Vlah9v8fs3jA== - -playwright@1.49.0: - version "1.49.0" - resolved "https://registry.npmjs.org/playwright/-/playwright-1.49.0.tgz" - integrity sha512-eKpmys0UFDnfNb3vfsf8Vx2LEOtflgRebl0Im2eQQnYMA4Aqd+Zw8bEOB+7ZKvN76901mRnqdsiOGKxzVTbi7A== - dependencies: - playwright-core "1.49.0" - optionalDependencies: - fsevents "2.3.2" - -prebuild-install@^7.1.1: - version "7.1.2" - resolved "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz" - integrity sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ== - dependencies: - detect-libc "^2.0.0" - expand-template "^2.0.3" - github-from-package "0.0.0" - minimist "^1.2.3" - mkdirp-classic "^0.5.3" - napi-build-utils "^1.0.1" - node-abi "^3.3.0" - pump "^3.0.0" - rc "^1.2.7" - simple-get "^4.0.0" - tar-fs "^2.0.0" - tunnel-agent "^0.6.0" - -promise-inflight@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz" - integrity sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g== - -promise-retry@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz" - integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g== - dependencies: - err-code "^2.0.2" - retry "^0.12.0" - -prompts@^2.4.2: - version "2.4.2" - resolved "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz" - integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== - dependencies: - kleur "^3.0.3" - sisteransi "^1.0.5" - -proxy-addr@~2.0.7: - version "2.0.7" - resolved "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz" - integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== - dependencies: - forwarded "0.2.0" - ipaddr.js "1.9.1" - -pstree.remy@^1.1.8: - version "1.1.8" - resolved "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz" - integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== - -pump@^3.0.0: - version "3.0.2" - resolved "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz" - integrity sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -qs@6.13.0: - version "6.13.0" - resolved "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz" - integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== - dependencies: - side-channel "^1.0.6" - -range-parser@~1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" - integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== - -raw-body@2.5.2: - version "2.5.2" - resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz" - integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== - dependencies: - bytes "3.1.2" - http-errors "2.0.0" - iconv-lite "0.4.24" - unpipe "1.0.0" - -rc@^1.2.7: - version "1.2.8" - resolved "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz" - integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== - dependencies: - deep-extend "^0.6.0" - ini "~1.3.0" - minimist "^1.2.0" - strip-json-comments "~2.0.1" - -readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0, readable-stream@^3.6.2: - version "3.6.2" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz" - integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readdirp@^4.0.1: - version "4.0.2" - resolved "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz" - integrity sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA== - -readdirp@~3.6.0: - version "3.6.0" - resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz" - integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== - dependencies: - picomatch "^2.2.1" - -rechoir@^0.8.0: - version "0.8.0" - resolved "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz" - integrity sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ== - dependencies: - resolve "^1.20.0" - -regexp-tree@~0.1.1: - version "0.1.27" - resolved "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz" - integrity sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA== - -require-from-string@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz" - integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== - -resolve-pkg-maps@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz" - integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== - -resolve@^1.20.0: - version "1.22.8" - resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz" - integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== - dependencies: - is-core-module "^2.13.0" - path-parse "^1.0.7" - supports-preserve-symlinks-flag "^1.0.0" - -restore-cursor@^5.0.0: - version "5.1.0" - resolved "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz" - integrity sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA== - dependencies: - onetime "^7.0.0" - signal-exit "^4.1.0" - -retry@^0.12.0: - version "0.12.0" - resolved "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz" - integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== - -rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - -safe-buffer@^5.0.1, safe-buffer@~5.2.0, safe-buffer@5.2.1: - version "5.2.1" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -safe-regex@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz" - integrity sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A== - dependencies: - regexp-tree "~0.1.1" - -safe-stable-stringify@^2.3.1: - version "2.5.0" - resolved "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz" - integrity sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA== - -"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@~2.1.0: - version "2.1.2" - resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -semver@^6.0.0: - version "6.3.1" - resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" - integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== - -semver@^7.3.5, semver@^7.5.3, semver@^7.6.3: - version "7.6.3" - resolved "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz" - integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== - -send@0.19.0: - version "0.19.0" - resolved "https://registry.npmjs.org/send/-/send-0.19.0.tgz" - integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== - 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" - -serve-static@1.16.2: - version "1.16.2" - resolved "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz" - integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== - dependencies: - encodeurl "~2.0.0" - escape-html "~1.0.3" - parseurl "~1.3.3" - send "0.19.0" - -set-blocking@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz" - integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== - -set-function-length@^1.2.2: - version "1.2.2" - resolved "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz" - integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== - dependencies: - define-data-property "^1.1.4" - es-errors "^1.3.0" - function-bind "^1.1.2" - get-intrinsic "^1.2.4" - gopd "^1.0.1" - has-property-descriptors "^1.0.2" - -setprototypeof@1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz" - integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== - -side-channel@^1.0.6: - version "1.0.6" - resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz" - integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== - dependencies: - call-bind "^1.0.7" - es-errors "^1.3.0" - get-intrinsic "^1.2.4" - object-inspect "^1.13.1" - -signal-exit@^3.0.0, signal-exit@^3.0.7: - version "3.0.7" - resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" - integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== - -signal-exit@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz" - integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== - -simple-concat@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz" - integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== - -simple-get@^4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz" - integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA== - dependencies: - decompress-response "^6.0.0" - once "^1.3.1" - simple-concat "^1.0.0" - -simple-swizzle@^0.2.2: - version "0.2.2" - resolved "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz" - integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg== - dependencies: - is-arrayish "^0.3.1" - -simple-update-notifier@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz" - integrity sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w== - dependencies: - semver "^7.5.3" - -sisteransi@^1.0.5: - version "1.0.5" - resolved "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz" - integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== - -smart-buffer@^4.2.0: - version "4.2.0" - resolved "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz" - integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== - -socks-proxy-agent@^6.0.0: - version "6.2.1" - resolved "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz" - integrity sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ== - dependencies: - agent-base "^6.0.2" - debug "^4.3.3" - socks "^2.6.2" - -socks@^2.6.2: - version "2.8.3" - resolved "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz" - integrity sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw== - dependencies: - ip-address "^9.0.5" - smart-buffer "^4.2.0" - -split-ca@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz" - integrity sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ== - -sprintf-js@^1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz" - integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== - -sqlite3@^5.1.7: - version "5.1.7" - resolved "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz" - integrity sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog== - dependencies: - bindings "^1.5.0" - node-addon-api "^7.0.0" - prebuild-install "^7.1.1" - tar "^6.1.11" - optionalDependencies: - node-gyp "8.x" - -ssh2@^1.15.0: - version "1.16.0" - resolved "https://registry.npmjs.org/ssh2/-/ssh2-1.16.0.tgz" - integrity sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg== - dependencies: - asn1 "^0.2.6" - bcrypt-pbkdf "^1.0.2" - optionalDependencies: - cpu-features "~0.0.10" - nan "^2.20.0" - -ssri@^8.0.0, ssri@^8.0.1: - version "8.0.1" - resolved "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz" - integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ== - dependencies: - minipass "^3.1.1" - -stack-trace@0.0.x: - version "0.0.10" - resolved "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz" - integrity sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg== - -statuses@2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" - integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== - -stdin-discarder@^0.2.2: - version "0.2.2" - resolved "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz" - integrity sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ== - -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^7.2.0: - version "7.2.0" - resolved "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz" - integrity sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ== - dependencies: - emoji-regex "^10.3.0" - get-east-asian-width "^1.0.0" - strip-ansi "^7.1.0" - -strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^7.1.0: - version "7.1.0" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz" - integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== - dependencies: - ansi-regex "^6.0.1" - -strip-bom@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz" - integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== - -strip-json-comments@~2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz" - integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== - -supports-color@^5.5.0: - version "5.5.0" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - -supports-preserve-symlinks-flag@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" - integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== - -swagger-jsdoc@^6.2.8: - version "6.2.8" - resolved "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz" - integrity sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ== - dependencies: - commander "6.2.0" - doctrine "3.0.0" - glob "7.1.6" - lodash.mergewith "^4.6.2" - swagger-parser "^10.0.3" - yaml "2.0.0-1" - -swagger-parser@^10.0.3: - version "10.0.3" - resolved "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz" - integrity sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg== - dependencies: - "@apidevtools/swagger-parser" "10.0.3" - -swagger-ui-dist@>=5.0.0: - version "5.18.2" - resolved "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.18.2.tgz" - integrity sha512-J+y4mCw/zXh1FOj5wGJvnAajq6XgHOyywsa9yITmwxIlJbMqITq3gYRZHaeqLVH/eV/HOPphE6NjF+nbSNC5Zw== - dependencies: - "@scarf/scarf" "=1.4.0" - -swagger-ui-express@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz" - integrity sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA== - dependencies: - swagger-ui-dist ">=5.0.0" - -tapable@^2.2.0, tapable@^2.2.1: - version "2.2.1" - resolved "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz" - integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== - -tar-fs@^2.0.0, tar-fs@~2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz" - integrity sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA== - dependencies: - chownr "^1.1.1" - mkdirp-classic "^0.5.2" - pump "^3.0.0" - tar-stream "^2.0.0" - -tar-stream@^2.0.0: - version "2.2.0" - resolved "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz" - integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== - dependencies: - bl "^4.0.3" - end-of-stream "^1.4.1" - fs-constants "^1.0.0" - inherits "^2.0.3" - readable-stream "^3.1.1" - -tar@^6.0.2, tar@^6.1.11, tar@^6.1.2: - version "6.2.1" - resolved "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz" - integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== - dependencies: - chownr "^2.0.0" - fs-minipass "^2.0.0" - minipass "^5.0.0" - minizlib "^2.1.1" - mkdirp "^1.0.3" - yallist "^4.0.0" - -teamcity-service-messages@^0.1.14: - version "0.1.14" - resolved "https://registry.npmjs.org/teamcity-service-messages/-/teamcity-service-messages-0.1.14.tgz" - integrity sha512-29aQwaHqm8RMX74u2o/h1KbMLP89FjNiMxD9wbF2BbWOnbM+q+d1sCEC+MqCc4QW3NJykn77OMpTFw/xTHIc0w== - -text-hex@1.0.x: - version "1.0.0" - resolved "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz" - integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg== - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -toidentifier@1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz" - integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== - -touch@^3.1.0: - version "3.1.1" - resolved "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz" - integrity sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA== - -tr46@~0.0.3: - version "0.0.3" - resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz" - integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== - -triple-beam@^1.3.0: - version "1.4.1" - resolved "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz" - integrity sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg== - -ts-node@^10.9.2: - version "10.9.2" - resolved "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz" - integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== - dependencies: - "@cspotcode/source-map-support" "^0.8.0" - "@tsconfig/node10" "^1.0.7" - "@tsconfig/node12" "^1.0.7" - "@tsconfig/node14" "^1.0.0" - "@tsconfig/node16" "^1.0.2" - acorn "^8.4.1" - acorn-walk "^8.1.1" - arg "^4.1.0" - create-require "^1.1.0" - diff "^4.0.1" - make-error "^1.1.1" - v8-compile-cache-lib "^3.0.1" - yn "3.1.1" - -tsconfig-paths-webpack-plugin@^4.2.0: - version "4.2.0" - resolved "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz" - integrity sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA== - dependencies: - chalk "^4.1.0" - enhanced-resolve "^5.7.0" - tapable "^2.2.1" - tsconfig-paths "^4.1.2" - -tsconfig-paths@^4.1.2: - version "4.2.0" - resolved "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz" - integrity sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg== - dependencies: - json5 "^2.2.2" - minimist "^1.2.6" - strip-bom "^3.0.0" - -tsx@^4.19.2: - version "4.19.2" - resolved "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz" - integrity sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g== - dependencies: - esbuild "~0.23.0" - get-tsconfig "^4.7.5" - optionalDependencies: - fsevents "~2.3.3" - -tunnel-agent@^0.6.0: - version "0.6.0" - resolved "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz" - integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w== - dependencies: - safe-buffer "^5.0.1" - -tweetnacl@^0.14.3: - version "0.14.5" - resolved "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz" - integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== - -type-is@~1.6.18: - version "1.6.18" - resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz" - integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== - dependencies: - media-typer "0.3.0" - mime-types "~2.1.24" - -typescript@>=2.7: - version "5.7.2" - resolved "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz" - integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg== - -uglify-js@^3.19.3: - version "3.19.3" - resolved "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz" - integrity sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ== - -undefsafe@^2.0.5: - version "2.0.5" - resolved "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz" - integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== - -undici-types@~5.26.4: - version "5.26.5" - resolved "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz" - integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== - -undici-types@~6.20.0: - version "6.20.0" - resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz" - integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== - -unique-filename@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz" - integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ== - dependencies: - unique-slug "^2.0.0" - -unique-slug@^2.0.0: - version "2.0.2" - resolved "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz" - integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w== - dependencies: - imurmurhash "^0.1.4" - -unpipe@~1.0.0, unpipe@1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" - integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== - -util-deprecate@^1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" - integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== - -utils-merge@1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz" - integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== - -v8-compile-cache-lib@^3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz" - integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== - -validator@^13.7.0: - version "13.12.0" - resolved "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz" - integrity sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg== - -vary@^1, vary@~1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" - integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== - -watskeburt@^4.1.1: - version "4.2.2" - resolved "https://registry.npmjs.org/watskeburt/-/watskeburt-4.2.2.tgz" - integrity sha512-AOCg1UYxWpiHW1tUwqpJau8vzarZYTtzl2uu99UptBmbzx6kOzCGMfRLF6KIRX4PYekmryn89MzxlRNkL66YyA== - -web-streams-polyfill@^3.0.3: - version "3.3.3" - resolved "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz" - integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw== - -webidl-conversions@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz" - integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== - -whatwg-url@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz" - integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== - dependencies: - tr46 "~0.0.3" - webidl-conversions "^3.0.0" - -which@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== - dependencies: - isexe "^2.0.0" - -wide-align@^1.1.2, wide-align@^1.1.5: - version "1.1.5" - resolved "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz" - integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== - dependencies: - string-width "^1.0.2 || 2 || 3 || 4" - -winston-transport@^4.9.0: - version "4.9.0" - resolved "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz" - integrity sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A== - dependencies: - logform "^2.7.0" - readable-stream "^3.6.2" - triple-beam "^1.3.0" - -winston@^3.15.0: - version "3.17.0" - resolved "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz" - integrity sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw== - dependencies: - "@colors/colors" "^1.6.0" - "@dabh/diagnostics" "^2.0.2" - async "^3.2.3" - is-stream "^2.0.0" - logform "^2.7.0" - one-time "^1.0.0" - readable-stream "^3.4.0" - safe-stable-stringify "^2.3.1" - stack-trace "0.0.x" - triple-beam "^1.3.0" - winston-transport "^4.9.0" - -wrappy@1: - version "1.0.2" - resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" - integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== - -yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== - -yaml@2.0.0-1: - version "2.0.0-1" - resolved "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz" - integrity sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ== - -yn@3.1.1: - version "3.1.1" - resolved "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz" - integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== - -z-schema@^5.0.1: - version "5.0.5" - resolved "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz" - integrity sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q== - dependencies: - lodash.get "^4.4.2" - lodash.isequal "^4.5.0" - validator "^13.7.0" - optionalDependencies: - commander "^9.4.1" From ff34dff392c284cd6339d9809acbcbb68411df76 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 30 Dec 2024 10:58:07 +0100 Subject: [PATCH 072/324] Fix: Added missing varaible.json file for workflow --- .github/workflows/validation.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml index d46610b..395569f 100644 --- a/.github/workflows/validation.yml +++ b/.github/workflows/validation.yml @@ -18,6 +18,9 @@ jobs: - name: Install dependencies run: npm ci --ignore-scripts + - name: Create varaibles.json + run: npm run local-env-file + - name: Run prettier run: npm run prettier From edaab084a2fc9644354c8d6dc056832c6fe0665e Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 30 Dec 2024 11:02:27 +0100 Subject: [PATCH 073/324] Chore: Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e24b149..4e6daf3 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ _Pipelines:_
[![Docker Image CI](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-image.yml/badge.svg?branch=main)](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-image.yml)
[![Build dockstatapi:nightly](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-dev.yaml/badge.svg?branch=dev)](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-dev.yaml)
+[![Tests](https://github.com/Its4Nik/dockstatapi/actions/workflows/validation.yml/badge.svg?branch=dev)](https://github.com/Its4Nik/dockstatapi/actions/workflows/validation.yml) This specific branch contains the currently WIP **DockStatAPI-v2**, this update will bring major breaking changes so please be careful. With this new release a couple of extra features (compared to v1) are going to be available. From d62abc09883dbcba17d1a6d1339bc9724ef8da1e Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 30 Dec 2024 11:12:56 +0100 Subject: [PATCH 074/324] Fix: Added "..." to workflow name --- .github/workflows/anchore.yml | 2 +- .github/workflows/build-dev.yaml | 2 +- .github/workflows/build-image.yml | 2 +- .github/workflows/build-test.yaml | 2 +- .github/workflows/cloc.yaml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/anchore.yml b/.github/workflows/anchore.yml index 2725a7c..c09b20e 100644 --- a/.github/workflows/anchore.yml +++ b/.github/workflows/anchore.yml @@ -1,4 +1,4 @@ -name: Anchore Grype vulnerability scan +name: "Anchore Grype vulnerability scan" on: [push] diff --git a/.github/workflows/build-dev.yaml b/.github/workflows/build-dev.yaml index f21ab4a..62e0da9 100644 --- a/.github/workflows/build-dev.yaml +++ b/.github/workflows/build-dev.yaml @@ -1,4 +1,4 @@ -name: Build dockstatapi:nightly +name: "Build dockstatapi:nightly" on: push: diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index 17933f9..9e382a2 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -1,4 +1,4 @@ -name: Build dockstatapi:latest +name: "Build dockstatapi:latest" on: release: diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index 2f2322f..631af23 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -1,4 +1,4 @@ -name: Build test docker image +name: "Build test docker image" on: [push] diff --git a/.github/workflows/cloc.yaml b/.github/workflows/cloc.yaml index d29afa4..004f51b 100644 --- a/.github/workflows/cloc.yaml +++ b/.github/workflows/cloc.yaml @@ -1,4 +1,4 @@ -name: Count Lines of Code +name: "Count Lines of Code" permissions: issues: write From 569044d224c4f98b2d1c05799f4f125750053806 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 30 Dec 2024 16:11:10 +0100 Subject: [PATCH 075/324] Feat: Add better workflow logic --- .github/workflows/{anchore.yml => anchore.yaml} | 3 ++- .github/workflows/{build-image.yml => build-image.yaml} | 0 .github/workflows/build-test.yaml | 3 ++- .github/workflows/{codeql.yml => codeql.yaml} | 7 +------ .../workflows/{remove-stale.yml => remove-stale.yaml} | 0 .github/workflows/{validation.yml => validation.yaml} | 9 +++++++++ 6 files changed, 14 insertions(+), 8 deletions(-) rename .github/workflows/{anchore.yml => anchore.yaml} (97%) rename .github/workflows/{build-image.yml => build-image.yaml} (100%) rename .github/workflows/{codeql.yml => codeql.yaml} (91%) rename .github/workflows/{remove-stale.yml => remove-stale.yaml} (100%) rename .github/workflows/{validation.yml => validation.yaml} (75%) diff --git a/.github/workflows/anchore.yml b/.github/workflows/anchore.yaml similarity index 97% rename from .github/workflows/anchore.yml rename to .github/workflows/anchore.yaml index c09b20e..290694c 100644 --- a/.github/workflows/anchore.yml +++ b/.github/workflows/anchore.yaml @@ -1,6 +1,7 @@ name: "Anchore Grype vulnerability scan" -on: [push] +on: + workflow_call: permissions: contents: read diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yaml similarity index 100% rename from .github/workflows/build-image.yml rename to .github/workflows/build-image.yaml diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index 631af23..a87c5d8 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -1,6 +1,7 @@ name: "Build test docker image" -on: [push] +on: + workflow_call: permissions: packages: write diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yaml similarity index 91% rename from .github/workflows/codeql.yml rename to .github/workflows/codeql.yaml index 081205c..d0eec67 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yaml @@ -1,12 +1,7 @@ name: "CodeQL Advanced" on: - push: - branches: ["main"] - pull_request: - branches: ["main"] - schedule: - - cron: "32 1 * * 5" + workflow_call: jobs: codeql: diff --git a/.github/workflows/remove-stale.yml b/.github/workflows/remove-stale.yaml similarity index 100% rename from .github/workflows/remove-stale.yml rename to .github/workflows/remove-stale.yaml diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yaml similarity index 75% rename from .github/workflows/validation.yml rename to .github/workflows/validation.yaml index 395569f..05f2377 100644 --- a/.github/workflows/validation.yml +++ b/.github/workflows/validation.yaml @@ -32,3 +32,12 @@ jobs: - name: Audit packages run: npm audit --audit-level=high + + - name: Run Anchore + uses: ./.github/workflows/anchore.yml + + - name: Run CodeQL + uses: ./.github/workflows/codeql.yml + + - name: Test build + uses: ./.github/workflows/build-test.yaml From 8183c6a648bd1712d1caf924b85d8923b3072eca Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 30 Dec 2024 16:12:33 +0100 Subject: [PATCH 076/324] Fix: Renamed files => adjusted in code --- .github/workflows/validation.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index 05f2377..eec6609 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -34,10 +34,10 @@ jobs: run: npm audit --audit-level=high - name: Run Anchore - uses: ./.github/workflows/anchore.yml + uses: ./.github/workflows/anchore.yaml - name: Run CodeQL - uses: ./.github/workflows/codeql.yml + uses: ./.github/workflows/codeql.yaml - name: Test build uses: ./.github/workflows/build-test.yaml From c646f0e349866a252d681894c785763a08adc1ac Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 30 Dec 2024 16:23:18 +0100 Subject: [PATCH 077/324] Fix: Workflows depending on each other (test) --- .github/workflows/anchore.yaml | 10 +++++++--- .github/workflows/build-test.yaml | 7 ++++++- .github/workflows/codeql.yaml | 10 +++++++--- .github/workflows/validation.yaml | 6 ------ 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/.github/workflows/anchore.yaml b/.github/workflows/anchore.yaml index 290694c..24ce218 100644 --- a/.github/workflows/anchore.yaml +++ b/.github/workflows/anchore.yaml @@ -1,7 +1,11 @@ -name: "Anchore Grype vulnerability scan" +name: "Anchore Grype Vulnerability Scan" on: - workflow_call: + workflow_run: + workflows: + - "Run all tests" # Replace with the actual name of the preceding workflow + types: + - completed permissions: contents: read @@ -16,7 +20,7 @@ jobs: - name: Download Grype run: | curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b $HOME/bin - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + - uses: actions/checkout@v4 - name: Build the Container image run: docker build . --file Dockerfile --tag localbuild/testimage:latest - name: Run Grype test diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index a87c5d8..5113169 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -1,7 +1,12 @@ name: "Build test docker image" on: - workflow_call: + workflow_run: + workflows: + - "Anchore Grype Vulnerability Scan" + - "CodeQL Advanced" + types: + - completed permissions: packages: write diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml index d0eec67..7ba4587 100644 --- a/.github/workflows/codeql.yaml +++ b/.github/workflows/codeql.yaml @@ -1,12 +1,16 @@ name: "CodeQL Advanced" on: - workflow_call: + workflow_run: + workflows: + - "Anchore Grype Vulnerability Scan" + types: + - completed jobs: codeql: name: Analyze TypeScript - runs-on: "ubuntu-latest" + runs-on: ubuntu-latest permissions: security-events: write packages: read @@ -44,4 +48,4 @@ jobs: - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 with: - category: "/language:${{matrix.language}}" + category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index eec6609..3c6f2ce 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -33,11 +33,5 @@ jobs: - name: Audit packages run: npm audit --audit-level=high - - name: Run Anchore - uses: ./.github/workflows/anchore.yaml - - - name: Run CodeQL - uses: ./.github/workflows/codeql.yaml - - name: Test build uses: ./.github/workflows/build-test.yaml From 19a1d242060aec391aa92e863cd404add8c7c178 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 30 Dec 2024 16:24:55 +0100 Subject: [PATCH 078/324] Fix: Remove old 'residue' of workflow files --- .github/workflows/validation.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index 3c6f2ce..395569f 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -32,6 +32,3 @@ jobs: - name: Audit packages run: npm audit --audit-level=high - - - name: Test build - uses: ./.github/workflows/build-test.yaml From 16b0aa90019c245666524b6780d6d3a2b7736862 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 30 Dec 2024 16:33:42 +0100 Subject: [PATCH 079/324] Fix: Adjust workflows --- .github/workflows/anchore.yaml | 8 +++ .github/workflows/build-test.yaml | 13 +++++ .github/workflows/codeql.yaml | 3 ++ CREDITS.md | 73 ++++++++++++-------------- package.json | 2 +- src/controllers/containerController.ts | 8 ++- 6 files changed, 62 insertions(+), 45 deletions(-) diff --git a/.github/workflows/anchore.yaml b/.github/workflows/anchore.yaml index 24ce218..436ba6b 100644 --- a/.github/workflows/anchore.yaml +++ b/.github/workflows/anchore.yaml @@ -17,15 +17,23 @@ jobs: steps: - name: Set up Grype installation path run: echo "$HOME/bin" >> $GITHUB_PATH + - name: Download Grype run: | curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b $HOME/bin + - uses: actions/checkout@v4 + - name: Build the Container image run: docker build . --file Dockerfile --tag localbuild/testimage:latest + - name: Run Grype test run: grype -o sarif localbuild/testimage:latest > results.sarif + - name: Upload Anchore scan SARIF report uses: github/codeql-action/upload-sarif@v3 with: sarif_file: ./results.sarif + + - name: Set Marker for Workflow Completion + run: echo "anchore_complete=true" >> $GITHUB_ENV diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index 5113169..a0f6db2 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -16,6 +16,19 @@ jobs: build-test: runs-on: ubuntu-latest steps: + - name: Check workflow dependencies + run: | + for i in {1..10}; do + if [[ "${{ env.anchore_complete }}" == "true" && "${{ env.codeql_complete }}" == "true" ]]; then + echo "All workflows complete!" + exit 0 + fi + echo "Dependencies not yet complete. Retrying in 60 seconds..." + sleep 60 + done + echo "Dependencies not met within the timeout period. Exiting." + exit 1 + - name: Checkout repository uses: actions/checkout@v4 diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml index 7ba4587..4e2f5f9 100644 --- a/.github/workflows/codeql.yaml +++ b/.github/workflows/codeql.yaml @@ -49,3 +49,6 @@ jobs: uses: github/codeql-action/analyze@v3 with: category: "/language:${{ matrix.language }}" + + - name: Set Marker for Workflow Completion + run: echo "codeql_complete=true" >> $GITHUB_ENV diff --git a/CREDITS.md b/CREDITS.md index be34b47..050b430 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -4,53 +4,48 @@ This file shows all npm packages used in DockStatAPI (also Dev packages) ### License: (MIT AND CC-BY-3.0) -| Name | Repository | Publisher | -|------|-------------|-----------| +| Name | Repository | Publisher | +| ----------------- | -------------------------------------------- | -------------------- | | spdx-ranges@2.1.1 | https://github.com/kemitchell/spdx-ranges.js | The Linux Foundation | - ### License: Apache-2.0 -| Name | Repository | Publisher | -|------|-------------|-----------| -| @balena/dockerignore@1.0.2 | https://github.com/balena-io-modules/dockerignore | N/A | -| @eslint/config-array@0.19.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/core@0.9.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/object-schema@2.1.5 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/plugin-kit@0.2.4 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @humanfs/core@0.19.1 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | -| @humanfs/node@0.16.6 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | -| @humanwhocodes/module-importer@1.0.1 | https://github.com/humanwhocodes/module-importer | Nicholas C. Zaks | -| @humanwhocodes/retry@0.3.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | -| @humanwhocodes/retry@0.4.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | -| @playwright/test@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | -| @scarf/scarf@1.4.0 | https://github.com/scarf-sh/scarf-js | Scarf Systems | -| detect-libc@2.0.3 | https://github.com/lovell/detect-libc | Lovell Fuller | -| docker-modem@5.0.3 | https://github.com/apocas/docker-modem | Pedro Dias | -| dockerode@4.0.2 | https://github.com/apocas/dockerode | Pedro Dias | -| doctrine@3.0.0 | https://github.com/eslint/doctrine | N/A | -| eslint-visitor-keys@3.4.3 | https://github.com/eslint/eslint-visitor-keys | Toru Nagashima | -| eslint-visitor-keys@4.2.0 | https://github.com/eslint/js | Toru Nagashima | -| playwright-core@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | -| playwright@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | -| spdx-correct@3.2.0 | https://github.com/jslicense/spdx-correct.js | N/A | -| swagger-ui-dist@5.18.2 | https://github.com/swagger-api/swagger-ui | N/A | -| tunnel-agent@0.6.0 | https://github.com/mikeal/tunnel-agent | Mikeal Rogers | -| typescript@5.7.2 | https://github.com/microsoft/TypeScript | Microsoft Corp. | -| validate-npm-package-license@3.0.4 | https://github.com/kemitchell/validate-npm-package-license.js | Kyle E. Mitchell | - +| Name | Repository | Publisher | +| ------------------------------------ | ------------------------------------------------------------- | --------------------- | +| @balena/dockerignore@1.0.2 | https://github.com/balena-io-modules/dockerignore | N/A | +| @eslint/config-array@0.19.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/core@0.9.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/object-schema@2.1.5 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/plugin-kit@0.2.4 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @humanfs/core@0.19.1 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | +| @humanfs/node@0.16.6 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | +| @humanwhocodes/module-importer@1.0.1 | https://github.com/humanwhocodes/module-importer | Nicholas C. Zaks | +| @humanwhocodes/retry@0.3.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | +| @humanwhocodes/retry@0.4.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | +| @playwright/test@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | +| @scarf/scarf@1.4.0 | https://github.com/scarf-sh/scarf-js | Scarf Systems | +| detect-libc@2.0.3 | https://github.com/lovell/detect-libc | Lovell Fuller | +| docker-modem@5.0.3 | https://github.com/apocas/docker-modem | Pedro Dias | +| dockerode@4.0.2 | https://github.com/apocas/dockerode | Pedro Dias | +| doctrine@3.0.0 | https://github.com/eslint/doctrine | N/A | +| eslint-visitor-keys@3.4.3 | https://github.com/eslint/eslint-visitor-keys | Toru Nagashima | +| eslint-visitor-keys@4.2.0 | https://github.com/eslint/js | Toru Nagashima | +| playwright-core@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | +| playwright@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | +| spdx-correct@3.2.0 | https://github.com/jslicense/spdx-correct.js | N/A | +| swagger-ui-dist@5.18.2 | https://github.com/swagger-api/swagger-ui | N/A | +| tunnel-agent@0.6.0 | https://github.com/mikeal/tunnel-agent | Mikeal Rogers | +| typescript@5.7.2 | https://github.com/microsoft/TypeScript | Microsoft Corp. | +| validate-npm-package-license@3.0.4 | https://github.com/kemitchell/validate-npm-package-license.js | Kyle E. Mitchell | ### License: CC-BY-3.0 -| Name | Repository | Publisher | -|------|-------------|-----------| +| Name | Repository | Publisher | +| --------------------- | -------------------------------------------------- | -------------------- | | spdx-exceptions@2.5.0 | https://github.com/kemitchell/spdx-exceptions.json | The Linux Foundation | - ### License: Python-2.0 -| Name | Repository | Publisher | -|------|-------------|-----------| -| argparse@2.0.1 | https://github.com/nodeca/argparse | N/A | - - +| Name | Repository | Publisher | +| -------------- | ---------------------------------- | --------- | +| argparse@2.0.1 | https://github.com/nodeca/argparse | N/A | diff --git a/package.json b/package.json index 9acd952..90422a2 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "docker:full": "docker compose up -d && [ -z \"$TMUX\" ] && tmux new-session -d -s docker 'docker compose logs -f master' \\; split-window -v 'docker compose logs -f slave' \\; attach-session || echo 'Already inside a tmux session. Exiting.'; docker compose down", "docker:build": "docker build . -t \"dockstatapi:local\" -f ./Dockerfile-dev && docker compose up -d", "docker:build:full": "npm run docker:build && [ -z \"$TMUX\" ] && tmux new-session -d -s docker 'docker compose up -d && docker compose logs -f master' \\; split-window -v 'docker compose logs -f slave' \\; attach-session || echo 'Already inside a tmux session. Exiting.'; docker compose down", - "prettier": "npx prettier -c ./src/**/*.ts --parser typescript --write && npx prettier -c ./.github/workflows/*.{yaml,yml} --parser yaml --write && npx prettier -c ./**/*.md --parser markdown --write && npx prettier -c ./**/*.json --parser json --write", + "prettier": "npx prettier -c ./src/**/*.ts --parser typescript --write && npx prettier -c ./.github/workflows/*.yaml --parser yaml --write && npx prettier -c ./**/*.md --parser markdown --write && npx prettier -c ./**/*.json --parser json --write", "lint": "npx eslint", "lint:fix": "npx eslint --fix", "license": "bash ./src/misc/credits.sh", diff --git a/src/controllers/containerController.ts b/src/controllers/containerController.ts index 8d3bef3..ef1c8ce 100644 --- a/src/controllers/containerController.ts +++ b/src/controllers/containerController.ts @@ -40,11 +40,9 @@ const getContainerStats = async ( logger.error( `Error fetching stats for container: ${containerID} from host: ${containerHost} - ${(error as Error).message}`, ); - res - .status(500) - .json({ - error: `Error fetching container stats: ${(error as Error).message}`, - }); + res.status(500).json({ + error: `Error fetching container stats: ${(error as Error).message}`, + }); } }; From 2766aa626e0f8243e89a508816b2e9b9c13fb687 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 30 Dec 2024 16:49:11 +0100 Subject: [PATCH 080/324] Fix: Adjust workflows (hopefully) --- .github/workflows/anchore.yaml | 6 +----- .github/workflows/build-test.yaml | 20 +------------------- .github/workflows/codeql.yaml | 6 +----- .github/workflows/validation.yaml | 17 +++++++++++++++++ 4 files changed, 20 insertions(+), 29 deletions(-) diff --git a/.github/workflows/anchore.yaml b/.github/workflows/anchore.yaml index 436ba6b..7aa9162 100644 --- a/.github/workflows/anchore.yaml +++ b/.github/workflows/anchore.yaml @@ -1,11 +1,7 @@ name: "Anchore Grype Vulnerability Scan" on: - workflow_run: - workflows: - - "Run all tests" # Replace with the actual name of the preceding workflow - types: - - completed + workflow_call: permissions: contents: read diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index a0f6db2..a87c5d8 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -1,12 +1,7 @@ name: "Build test docker image" on: - workflow_run: - workflows: - - "Anchore Grype Vulnerability Scan" - - "CodeQL Advanced" - types: - - completed + workflow_call: permissions: packages: write @@ -16,19 +11,6 @@ jobs: build-test: runs-on: ubuntu-latest steps: - - name: Check workflow dependencies - run: | - for i in {1..10}; do - if [[ "${{ env.anchore_complete }}" == "true" && "${{ env.codeql_complete }}" == "true" ]]; then - echo "All workflows complete!" - exit 0 - fi - echo "Dependencies not yet complete. Retrying in 60 seconds..." - sleep 60 - done - echo "Dependencies not met within the timeout period. Exiting." - exit 1 - - name: Checkout repository uses: actions/checkout@v4 diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml index 4e2f5f9..c187b09 100644 --- a/.github/workflows/codeql.yaml +++ b/.github/workflows/codeql.yaml @@ -1,11 +1,7 @@ name: "CodeQL Advanced" on: - workflow_run: - workflows: - - "Anchore Grype Vulnerability Scan" - types: - - completed + workflow_call: jobs: codeql: diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index 395569f..1918efd 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -32,3 +32,20 @@ jobs: - name: Audit packages run: npm audit --audit-level=high + + code-testing: + needs: validation + runs-on: ubuntu-latest + steps: + - name: CodeQL + uses: ./.github/workflows/codeql.yaml + + - name: Anchore + uses: ./.github/workflows/anchore.yaml + + test-building: + needs: code-testing + runs-on: ubuntu-latest + steps: + - name: Test build + uses: ./.github/workflows/build-test.yaml From 406fe0cfd7990a807e5fec8e9bba16a27c387205 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 30 Dec 2024 16:52:21 +0100 Subject: [PATCH 081/324] Fix: More adjustments --- .github/workflows/validation.yaml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index 1918efd..9a6387e 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -33,18 +33,28 @@ jobs: - name: Audit packages run: npm audit --audit-level=high - code-testing: + CodeQL: needs: validation runs-on: ubuntu-latest steps: + - name: Checkout + uses: actions/checkout@v4 + - name: CodeQL uses: ./.github/workflows/codeql.yaml + Anchore: + needs: validation + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Anchore uses: ./.github/workflows/anchore.yaml test-building: - needs: code-testing + needs: [CodeQL, Anchore] runs-on: ubuntu-latest steps: - name: Test build From 25696d8f44d726e495d31a256ed1ef41ee98548d Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 30 Dec 2024 17:08:37 +0100 Subject: [PATCH 082/324] Fix: Move workflow files around --- .github/workflows/anchore.yaml | 35 ---------- .github/workflows/build-test.yaml | 45 ------------ .github/workflows/codeql.yaml | 50 -------------- .github/workflows/validation.yaml | 109 +++++++++++++++++++++++++++--- 4 files changed, 100 insertions(+), 139 deletions(-) delete mode 100644 .github/workflows/anchore.yaml delete mode 100644 .github/workflows/codeql.yaml diff --git a/.github/workflows/anchore.yaml b/.github/workflows/anchore.yaml deleted file mode 100644 index 7aa9162..0000000 --- a/.github/workflows/anchore.yaml +++ /dev/null @@ -1,35 +0,0 @@ -name: "Anchore Grype Vulnerability Scan" - -on: - workflow_call: - -permissions: - contents: read - security-events: write - -jobs: - anchore: - runs-on: ubuntu-latest - steps: - - name: Set up Grype installation path - run: echo "$HOME/bin" >> $GITHUB_PATH - - - name: Download Grype - run: | - curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b $HOME/bin - - - uses: actions/checkout@v4 - - - name: Build the Container image - run: docker build . --file Dockerfile --tag localbuild/testimage:latest - - - name: Run Grype test - run: grype -o sarif localbuild/testimage:latest > results.sarif - - - name: Upload Anchore scan SARIF report - uses: github/codeql-action/upload-sarif@v3 - with: - sarif_file: ./results.sarif - - - name: Set Marker for Workflow Completion - run: echo "anchore_complete=true" >> $GITHUB_ENV diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index a87c5d8..5ea7436 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -10,48 +10,3 @@ permissions: jobs: build-test: runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - # Set up Node.js using nvm - - name: Set up Node.js version from .nvmrc - run: | - curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash - export NVM_DIR="$HOME/.nvm" - [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" - nvm install - nvm use - node -v - npm -v - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to Github Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Generate Docker tags - uses: docker/metadata-action@v5 - id: metadata - with: - images: ghcr.io/${{ github.repository }} - tags: | - type=raw,enable=true,priority=200,prefix=,suffix=,value=${{ github.sha }} - - - name: Build and Push Docker Images - uses: docker/build-push-action@v6 - with: - platforms: linux/amd64,linux/arm64 - push: false - tags: ${{ steps.metadata.outputs.tags }} - labels: ${{ steps.metadata.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml deleted file mode 100644 index c187b09..0000000 --- a/.github/workflows/codeql.yaml +++ /dev/null @@ -1,50 +0,0 @@ -name: "CodeQL Advanced" - -on: - workflow_call: - -jobs: - codeql: - name: Analyze TypeScript - runs-on: ubuntu-latest - permissions: - security-events: write - packages: read - actions: read - contents: read - - strategy: - fail-fast: false - matrix: - include: - - language: javascript-typescript - build-mode: none - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - build-mode: ${{ matrix.build-mode }} - queries: security-extended - - - if: matrix.build-mode == 'manual' - shell: bash - run: | - echo 'If you are using a "manual" build mode for one or more of the' \ - 'languages you are analyzing, replace this with the commands to build' \ - 'your code, for example:' - echo ' make bootstrap' - echo ' make release' - exit 1 - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: "/language:${{ matrix.language }}" - - - name: Set Marker for Workflow Completion - run: echo "codeql_complete=true" >> $GITHUB_ENV diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index 9a6387e..a0154c4 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -36,26 +36,117 @@ jobs: CodeQL: needs: validation runs-on: ubuntu-latest + name: Analyze TypeScript + permissions: + security-events: write + packages: read + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: javascript-typescript + build-mode: none + steps: - - name: Checkout + - name: Checkout repository uses: actions/checkout@v4 - - name: CodeQL - uses: ./.github/workflows/codeql.yaml + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + queries: security-extended + + - if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{ matrix.language }}" + + - name: Set Marker for Workflow Completion + run: echo "codeql_complete=true" >> $GITHUB_ENV Anchore: needs: validation runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v4 + - name: Set up Grype installation path + run: echo "$HOME/bin" >> $GITHUB_PATH + + - name: Download Grype + run: | + curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b $HOME/bin + + - uses: actions/checkout@v4 + + - name: Build the Container image + run: docker build . --file Dockerfile --tag localbuild/testimage:latest - - name: Anchore - uses: ./.github/workflows/anchore.yaml + - name: Run Grype test + run: grype -o sarif localbuild/testimage:latest > results.sarif + + - name: Upload Anchore scan SARIF report + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: ./results.sarif test-building: needs: [CodeQL, Anchore] runs-on: ubuntu-latest steps: - - name: Test build - uses: ./.github/workflows/build-test.yaml + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js version from .nvmrc + run: | + curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash + export NVM_DIR="$HOME/.nvm" + [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" + nvm install + nvm use + node -v + npm -v + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Github Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Generate Docker tags + uses: docker/metadata-action@v5 + id: metadata + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=raw,enable=true,priority=200,prefix=,suffix=,value=${{ github.sha }} + + - name: Build and Push Docker Images + uses: docker/build-push-action@v6 + with: + platforms: linux/amd64,linux/arm64 + push: false + tags: ${{ steps.metadata.outputs.tags }} + labels: ${{ steps.metadata.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max From 7a4134fdefa731817cb5669b8331f6dd776f29a8 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 30 Dec 2024 17:15:48 +0100 Subject: [PATCH 083/324] Fix: Fixing permissions --- .github/workflows/validation.yaml | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index a0154c4..6bc1550 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -5,6 +5,11 @@ on: [push] jobs: validation: runs-on: ubuntu-latest + permissions: + security-events: write + packages: read + actions: read + contents: read steps: - name: Checkout uses: actions/checkout@v4 @@ -76,12 +81,14 @@ jobs: with: category: "/language:${{ matrix.language }}" - - name: Set Marker for Workflow Completion - run: echo "codeql_complete=true" >> $GITHUB_ENV - Anchore: needs: validation runs-on: ubuntu-latest + permissions: + security-events: write + packages: read + actions: read + contents: read steps: - name: Set up Grype installation path run: echo "$HOME/bin" >> $GITHUB_PATH @@ -106,6 +113,11 @@ jobs: test-building: needs: [CodeQL, Anchore] runs-on: ubuntu-latest + permissions: + security-events: write + packages: read + actions: read + contents: read steps: - name: Checkout repository uses: actions/checkout@v4 From 61a49457e7e47f9ca45b00253209711adfece650 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 30 Dec 2024 17:26:17 +0100 Subject: [PATCH 084/324] Feat: Added workflow naming --- .github/workflows/build-test.yaml | 12 ------------ .github/workflows/validation.yaml | 6 +++++- 2 files changed, 5 insertions(+), 13 deletions(-) delete mode 100644 .github/workflows/build-test.yaml diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml deleted file mode 100644 index 5ea7436..0000000 --- a/.github/workflows/build-test.yaml +++ /dev/null @@ -1,12 +0,0 @@ -name: "Build test docker image" - -on: - workflow_call: - -permissions: - packages: write - contents: read - -jobs: - build-test: - runs-on: ubuntu-latest diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index 6bc1550..9d771ad 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -5,6 +5,7 @@ on: [push] jobs: validation: runs-on: ubuntu-latest + name: Validation permissions: security-events: write packages: read @@ -66,7 +67,8 @@ jobs: build-mode: ${{ matrix.build-mode }} queries: security-extended - - if: matrix.build-mode == 'manual' + - name: Check build mode + if: matrix.build-mode == 'manual' shell: bash run: | echo 'If you are using a "manual" build mode for one or more of the' \ @@ -84,6 +86,7 @@ jobs: Anchore: needs: validation runs-on: ubuntu-latest + name: Anchore permissions: security-events: write packages: read @@ -113,6 +116,7 @@ jobs: test-building: needs: [CodeQL, Anchore] runs-on: ubuntu-latest + name: Test building permissions: security-events: write packages: read From ee37b8071e2393db94fa03208b25913ef82eeeff Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 30 Dec 2024 17:43:09 +0100 Subject: [PATCH 085/324] Fix: Preparing for yet another workflow change... --- .github/workflows/validation.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index 9d771ad..5d6e8d3 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -114,7 +114,7 @@ jobs: sarif_file: ./results.sarif test-building: - needs: [CodeQL, Anchore] + needs: validation runs-on: ubuntu-latest name: Test building permissions: From a63c4b0258b9d62efc063578c2e455591bdfa677 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 31 Dec 2024 15:05:31 +0100 Subject: [PATCH 086/324] Feat: New workflow structure for dev build --- .github/workflows/build-dev.yaml | 47 ----------------------------- .github/workflows/validation.yaml | 49 +++++++++++++++++++++++++++---- 2 files changed, 44 insertions(+), 52 deletions(-) delete mode 100644 .github/workflows/build-dev.yaml diff --git a/.github/workflows/build-dev.yaml b/.github/workflows/build-dev.yaml deleted file mode 100644 index 62e0da9..0000000 --- a/.github/workflows/build-dev.yaml +++ /dev/null @@ -1,47 +0,0 @@ -name: "Build dockstatapi:nightly" - -on: - push: - branches: - - "dev" - -permissions: - packages: write - contents: read - -jobs: - build-dev: - runs-on: ubuntu-latest - steps: - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to Github Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ github.token }} - - - name: Generate Docker tags - uses: docker/metadata-action@v5 - id: metadata - with: - images: ghcr.io/${{ github.repository }} - tags: | - type=raw,enable=true,priority=200,prefix=,suffix=,value=nightly - flavor: | - latest=false - - - name: Build and push - uses: docker/build-push-action@v6 - with: - platforms: linux/amd64,linux/arm64, - push: true - tags: ${{ steps.metadata.outputs.tags }} - labels: ${{ steps.metadata.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index 5d6e8d3..501ac9c 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -5,7 +5,7 @@ on: [push] jobs: validation: runs-on: ubuntu-latest - name: Validation + name: "Validation" permissions: security-events: write packages: read @@ -42,7 +42,7 @@ jobs: CodeQL: needs: validation runs-on: ubuntu-latest - name: Analyze TypeScript + name: "Analyze TypeScript" permissions: security-events: write packages: read @@ -86,7 +86,7 @@ jobs: Anchore: needs: validation runs-on: ubuntu-latest - name: Anchore + name: "Anchore" permissions: security-events: write packages: read @@ -114,9 +114,9 @@ jobs: sarif_file: ./results.sarif test-building: - needs: validation + needs: [validation, Anchore, CodeQL] runs-on: ubuntu-latest - name: Test building + name: "Test building" permissions: security-events: write packages: read @@ -166,3 +166,42 @@ jobs: labels: ${{ steps.metadata.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max + + build-dev: + name: "Dev-build" + runs-on: ubuntu-latest + if: github.ref_name == 'dev' + needs: [validation, test-building, Anchore, CodeQL] + steps: + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Github Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ github.token }} + + - name: Generate Docker tags + uses: docker/metadata-action@v5 + id: metadata + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=raw,enable=true,priority=200,prefix=,suffix=,value=nightly + flavor: | + latest=false + + - name: Build and push + uses: docker/build-push-action@v6 + with: + platforms: linux/amd64,linux/arm64, + push: true + tags: ${{ steps.metadata.outputs.tags }} + labels: ${{ steps.metadata.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max From 3bf8da473822f54eacef67083052f54f589712dc Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 31 Dec 2024 15:10:09 +0100 Subject: [PATCH 087/324] Fix: permissions --- .github/workflows/validation.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index 501ac9c..dd7357c 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -169,6 +169,11 @@ jobs: build-dev: name: "Dev-build" + permissions: + security-events: read + packages: write + actions: read + contents: read runs-on: ubuntu-latest if: github.ref_name == 'dev' needs: [validation, test-building, Anchore, CodeQL] From 687e44aa0320d169d6a2a11280872228d58fbc55 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 31 Dec 2024 15:14:50 +0100 Subject: [PATCH 088/324] Feat: Update to ubuntu-24.04 in GHA --- .github/workflows/build-image.yaml | 2 +- .github/workflows/cloc.yaml | 2 +- .github/workflows/remove-stale.yaml | 2 +- .github/workflows/validation.yaml | 10 +++++----- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build-image.yaml b/.github/workflows/build-image.yaml index 9e382a2..720bed8 100644 --- a/.github/workflows/build-image.yaml +++ b/.github/workflows/build-image.yaml @@ -10,7 +10,7 @@ permissions: jobs: build-release: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Set up QEMU uses: docker/setup-qemu-action@v3 diff --git a/.github/workflows/cloc.yaml b/.github/workflows/cloc.yaml index 004f51b..0f4245f 100644 --- a/.github/workflows/cloc.yaml +++ b/.github/workflows/cloc.yaml @@ -10,7 +10,7 @@ on: jobs: cloc: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/remove-stale.yaml b/.github/workflows/remove-stale.yaml index 93d1acd..47f9ae2 100644 --- a/.github/workflows/remove-stale.yaml +++ b/.github/workflows/remove-stale.yaml @@ -5,7 +5,7 @@ on: jobs: remove-stale: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/stale@v9 with: diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index dd7357c..1abe640 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -4,7 +4,7 @@ on: [push] jobs: validation: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 name: "Validation" permissions: security-events: write @@ -41,7 +41,7 @@ jobs: CodeQL: needs: validation - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 name: "Analyze TypeScript" permissions: security-events: write @@ -85,7 +85,7 @@ jobs: Anchore: needs: validation - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 name: "Anchore" permissions: security-events: write @@ -115,7 +115,7 @@ jobs: test-building: needs: [validation, Anchore, CodeQL] - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 name: "Test building" permissions: security-events: write @@ -174,7 +174,7 @@ jobs: packages: write actions: read contents: read - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 if: github.ref_name == 'dev' needs: [validation, test-building, Anchore, CodeQL] steps: From dc2fed835e24c68d3fe72f70f3be306ace28a754 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 1 Jan 2025 01:17:16 +0100 Subject: [PATCH 089/324] Feat: Custom User for Docker container --- Dockerfile | 25 ++++++++++++------------- Dockerfile-dev | 25 ++++++++++++------------- docker-compose.yaml | 5 ++++- src/misc/createEnvFile.sh | 0 src/misc/credits.sh | 0 src/misc/entrypoint.sh | 3 ++- src/misc/minifyDist.sh | 0 7 files changed, 30 insertions(+), 28 deletions(-) mode change 100644 => 100755 src/misc/createEnvFile.sh mode change 100644 => 100755 src/misc/credits.sh mode change 100644 => 100755 src/misc/minifyDist.sh diff --git a/Dockerfile b/Dockerfile index dc4f58c..0e59a45 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,10 +14,7 @@ LABEL org.opencontainers.image.source="https://github.com/its4nik/dockstatapi" WORKDIR /build ENV NODE_NO_WARNINGS=1 -RUN apk update && \ - apk upgrade && \ - apk add bash - +RUN apk add --update --no-cache bash COPY tsconfig.json environment.d.ts package*.json tsconfig.json ./ RUN npm install @@ -29,16 +26,13 @@ RUN npm run build:mini # Stage 2: main stage FROM alpine AS main -# Needed packages -RUN apk update && \ - apk upgrade && \ - apk add --update npm +RUN apk add --update npm WORKDIR /build RUN mkdir -p /build/src/data -COPY tsconfig.json environment.d.ts package*.json tsconfig.json ./ +COPY package*.json ./ RUN npm install --omit=dev COPY --from=builder /build/dist/* /build/src @@ -50,13 +44,18 @@ RUN node src/config/db.js # Stage 3: Production stage FROM alpine AS production -RUN apk add --update bash curl nodejs +WORKDIR /api + +RUN apk add --update --no-cache bash curl nodejs && \ + adduser -h /api -s /bin/bash -D dockstatapi dockstatapi && \ + chown -hR dockstatapi:dockstatapi /api + +USER dockstatapi + HEALTHCHECK --interval=5m --timeout=3s \ CMD curl -f http://localhost:9876/api/status || exit 1 -WORKDIR /api - -COPY --from=main /build /api +COPY --chown=dockstatapi:dockstatapi --from=main /build /api EXPOSE 9876 ENTRYPOINT [ "bash", "./entrypoint.sh" ] diff --git a/Dockerfile-dev b/Dockerfile-dev index bd24688..ba9c01c 100644 --- a/Dockerfile-dev +++ b/Dockerfile-dev @@ -14,10 +14,7 @@ LABEL org.opencontainers.image.source="https://github.com/its4nik/dockstatapi" WORKDIR /build ENV NODE_NO_WARNINGS=1 -RUN apk update && \ - apk upgrade && \ - apk add bash - +RUN apk add --update --no-cache bash COPY tsconfig.json environment.d.ts package*.json tsconfig.json ./ RUN npm install @@ -29,16 +26,13 @@ RUN npm run build # Stage 2: main stage FROM alpine AS main -# Needed packages -RUN apk update && \ - apk upgrade && \ - apk add --update npm +RUN apk add --update npm WORKDIR /build RUN mkdir -p /build/src/data -COPY tsconfig.json environment.d.ts package*.json tsconfig.json ./ +COPY package*.json ./ RUN npm install --omit=dev COPY --from=builder /build/dist/* /build/src @@ -50,13 +44,18 @@ RUN node src/config/db.js # Stage 3: Production stage FROM alpine AS production -RUN apk add --update bash curl nodejs +WORKDIR /api + +RUN apk add --update --no-cache bash curl nodejs && \ + adduser -h /api -s /bin/bash -D dockstatapi dockstatapi && \ + chown -hR dockstatapi:dockstatapi /api + +USER dockstatapi + HEALTHCHECK --interval=5m --timeout=3s \ CMD curl -f http://localhost:9876/api/status || exit 1 -WORKDIR /api - -COPY --from=main /build /api +COPY --chown=dockstatapi:dockstatapi --from=main /build /api EXPOSE 9876 ENTRYPOINT [ "bash", "./entrypoint.sh" ] diff --git a/docker-compose.yaml b/docker-compose.yaml index 06d1f45..4789b71 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -7,7 +7,7 @@ services: container_name: master environment: - NODE_ENV=development - - HA_MASTER=true + - HA_MASTER=false - HA_MASTER_IP=master:9876 - HA_NODE=slave:9876 - HA_UNSAFE=true @@ -21,6 +21,7 @@ services: depends_on: - slave - test-socket-proxy + slave: container_name: slave environment: @@ -30,6 +31,8 @@ services: ports: - 6789:9876 image: dockstatapi:local + depends_on: + - test-socket-proxy networks: - shared-network diff --git a/src/misc/createEnvFile.sh b/src/misc/createEnvFile.sh old mode 100644 new mode 100755 diff --git a/src/misc/credits.sh b/src/misc/credits.sh old mode 100644 new mode 100755 diff --git a/src/misc/entrypoint.sh b/src/misc/entrypoint.sh index 83eaf46..60b8a0e 100755 --- a/src/misc/entrypoint.sh +++ b/src/misc/entrypoint.sh @@ -1,3 +1,4 @@ +# entrypoint.sh: #!/bin/bash VERSION="$(cat ./package.json | grep version | cut -d '"' -f 4)" @@ -24,6 +25,6 @@ DockStat and DockStatAPI are 2 fully OpenSource projects, DockStatAPI is a simpl " -bash "./createEnvFile.sh" +bash ./createEnvFile.sh exec node src/server.js diff --git a/src/misc/minifyDist.sh b/src/misc/minifyDist.sh old mode 100644 new mode 100755 From 1f59fd4b5428f4151b87b93e7c484247ca178fd3 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 1 Jan 2025 01:57:41 +0100 Subject: [PATCH 090/324] Fix: Lock acquisition needs exponential backoff to prevent thundering herd Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- src/controllers/highAvailability.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/controllers/highAvailability.ts b/src/controllers/highAvailability.ts index 7bf7dc7..20e33de 100644 --- a/src/controllers/highAvailability.ts +++ b/src/controllers/highAvailability.ts @@ -47,10 +47,25 @@ const configFiles: string[] = [ "./src/data/password.json", ]; +const MAX_RETRIES = 10; +const BASE_DELAY_MS = 100; + async function acquireLock(): Promise { + let retryCount = 0; + while (fs.existsSync(lockFilePath)) { - logger.warn("Lock file exists, waiting..."); - await sleep(100); + if (retryCount >= MAX_RETRIES) { + throw new Error("Failed to acquire lock: maximum retry attempts exceeded"); + } + + const backoffMs = BASE_DELAY_MS * Math.pow(2, retryCount); + // Add jitter to prevent thundering herd + const jitter = Math.random() * 0.3 * backoffMs; + const delayMs = backoffMs + jitter; + + logger.warn(`Lock file exists, waiting ${Math.round(delayMs)}ms before retry ${retryCount + 1}/${MAX_RETRIES}...`); + await sleep(delayMs); + retryCount++; } try { From a18b9d47158caa19617081c4ce69e4c4d49c758c Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 1 Jan 2025 02:00:48 +0100 Subject: [PATCH 091/324] Feat: Daily rotating log files --- .github/workflows/validation.yaml | 4 +-- package-lock.json | 48 ++++++++++++++++++++++++++++++- package.json | 3 +- src/config/loggerConfig.ts | 9 +++++- 4 files changed, 59 insertions(+), 5 deletions(-) diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index 1abe640..dfd9330 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -63,7 +63,7 @@ jobs: - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: - languages: ${{ matrix.language }} + languages: javascript-typescript build-mode: ${{ matrix.build-mode }} queries: security-extended @@ -81,7 +81,7 @@ jobs: - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 with: - category: "/language:${{ matrix.language }}" + category: "/language:javascript-typescript" Anchore: needs: validation diff --git a/package-lock.json b/package-lock.json index ba55c01..1310ab8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,8 @@ "sqlite3": "^5.1.7", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1", - "winston": "^3.15.0" + "winston": "^3.15.0", + "winston-daily-rotate-file": "^5.0.0" }, "devDependencies": { "@eslint/js": "^9.17.0", @@ -3031,6 +3032,15 @@ "node": ">=16.0.0" } }, + "node_modules/file-stream-rotator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.6.1.tgz", + "integrity": "sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.1" + } + }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -4512,6 +4522,15 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "license": "MIT" }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -4869,6 +4888,15 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", @@ -6980,6 +7008,24 @@ "node": ">= 12.0.0" } }, + "node_modules/winston-daily-rotate-file": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-5.0.0.tgz", + "integrity": "sha512-JDjiXXkM5qvwY06733vf09I2wnMXpZEhxEVOSPenZMii+g7pcDcTBt2MRugnoi8BwVSuCT2jfRXBUy+n1Zz/Yw==", + "license": "MIT", + "dependencies": { + "file-stream-rotator": "^0.6.1", + "object-hash": "^3.0.0", + "triple-beam": "^1.4.1", + "winston-transport": "^4.7.0" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "winston": "^3" + } + }, "node_modules/winston-transport": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", diff --git a/package.json b/package.json index 90422a2..6dd43ab 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,8 @@ "sqlite3": "^5.1.7", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1", - "winston": "^3.15.0" + "winston": "^3.15.0", + "winston-daily-rotate-file": "^5.0.0" }, "devDependencies": { "@eslint/js": "^9.17.0", diff --git a/src/config/loggerConfig.ts b/src/config/loggerConfig.ts index 5d1a33e..f2b30f4 100644 --- a/src/config/loggerConfig.ts +++ b/src/config/loggerConfig.ts @@ -1,4 +1,5 @@ import { createLogger, format, transports } from "winston"; +import DailyRotateFile from "winston-daily-rotate-file"; const gray = "\x1b[90m"; const reset = "\x1b[0m"; @@ -49,7 +50,13 @@ const logger = createLogger({ ), transports: [ new transports.Console(), - new transports.File({ filename: "logs/app.log" }), + new DailyRotateFile({ + filename: "logs/app-%DATE%.log", + datePattern: "YYYY-MM-DD", + maxSize: "20m", + maxFiles: "14d", + zippedArchive: true, + }), ], }); From a6838b08fe1442b624caa6466a9f00063ab32794 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 1 Jan 2025 02:01:49 +0100 Subject: [PATCH 092/324] Feat: Better locking --- CREDITS.md | 73 +++++++++++++++-------------- src/controllers/highAvailability.ts | 8 +++- 2 files changed, 45 insertions(+), 36 deletions(-) diff --git a/CREDITS.md b/CREDITS.md index 050b430..be34b47 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -4,48 +4,53 @@ This file shows all npm packages used in DockStatAPI (also Dev packages) ### License: (MIT AND CC-BY-3.0) -| Name | Repository | Publisher | -| ----------------- | -------------------------------------------- | -------------------- | +| Name | Repository | Publisher | +|------|-------------|-----------| | spdx-ranges@2.1.1 | https://github.com/kemitchell/spdx-ranges.js | The Linux Foundation | + ### License: Apache-2.0 -| Name | Repository | Publisher | -| ------------------------------------ | ------------------------------------------------------------- | --------------------- | -| @balena/dockerignore@1.0.2 | https://github.com/balena-io-modules/dockerignore | N/A | -| @eslint/config-array@0.19.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/core@0.9.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/object-schema@2.1.5 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/plugin-kit@0.2.4 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @humanfs/core@0.19.1 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | -| @humanfs/node@0.16.6 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | -| @humanwhocodes/module-importer@1.0.1 | https://github.com/humanwhocodes/module-importer | Nicholas C. Zaks | -| @humanwhocodes/retry@0.3.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | -| @humanwhocodes/retry@0.4.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | -| @playwright/test@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | -| @scarf/scarf@1.4.0 | https://github.com/scarf-sh/scarf-js | Scarf Systems | -| detect-libc@2.0.3 | https://github.com/lovell/detect-libc | Lovell Fuller | -| docker-modem@5.0.3 | https://github.com/apocas/docker-modem | Pedro Dias | -| dockerode@4.0.2 | https://github.com/apocas/dockerode | Pedro Dias | -| doctrine@3.0.0 | https://github.com/eslint/doctrine | N/A | -| eslint-visitor-keys@3.4.3 | https://github.com/eslint/eslint-visitor-keys | Toru Nagashima | -| eslint-visitor-keys@4.2.0 | https://github.com/eslint/js | Toru Nagashima | -| playwright-core@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | -| playwright@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | -| spdx-correct@3.2.0 | https://github.com/jslicense/spdx-correct.js | N/A | -| swagger-ui-dist@5.18.2 | https://github.com/swagger-api/swagger-ui | N/A | -| tunnel-agent@0.6.0 | https://github.com/mikeal/tunnel-agent | Mikeal Rogers | -| typescript@5.7.2 | https://github.com/microsoft/TypeScript | Microsoft Corp. | -| validate-npm-package-license@3.0.4 | https://github.com/kemitchell/validate-npm-package-license.js | Kyle E. Mitchell | +| Name | Repository | Publisher | +|------|-------------|-----------| +| @balena/dockerignore@1.0.2 | https://github.com/balena-io-modules/dockerignore | N/A | +| @eslint/config-array@0.19.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/core@0.9.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/object-schema@2.1.5 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/plugin-kit@0.2.4 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @humanfs/core@0.19.1 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | +| @humanfs/node@0.16.6 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | +| @humanwhocodes/module-importer@1.0.1 | https://github.com/humanwhocodes/module-importer | Nicholas C. Zaks | +| @humanwhocodes/retry@0.3.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | +| @humanwhocodes/retry@0.4.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | +| @playwright/test@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | +| @scarf/scarf@1.4.0 | https://github.com/scarf-sh/scarf-js | Scarf Systems | +| detect-libc@2.0.3 | https://github.com/lovell/detect-libc | Lovell Fuller | +| docker-modem@5.0.3 | https://github.com/apocas/docker-modem | Pedro Dias | +| dockerode@4.0.2 | https://github.com/apocas/dockerode | Pedro Dias | +| doctrine@3.0.0 | https://github.com/eslint/doctrine | N/A | +| eslint-visitor-keys@3.4.3 | https://github.com/eslint/eslint-visitor-keys | Toru Nagashima | +| eslint-visitor-keys@4.2.0 | https://github.com/eslint/js | Toru Nagashima | +| playwright-core@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | +| playwright@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | +| spdx-correct@3.2.0 | https://github.com/jslicense/spdx-correct.js | N/A | +| swagger-ui-dist@5.18.2 | https://github.com/swagger-api/swagger-ui | N/A | +| tunnel-agent@0.6.0 | https://github.com/mikeal/tunnel-agent | Mikeal Rogers | +| typescript@5.7.2 | https://github.com/microsoft/TypeScript | Microsoft Corp. | +| validate-npm-package-license@3.0.4 | https://github.com/kemitchell/validate-npm-package-license.js | Kyle E. Mitchell | + ### License: CC-BY-3.0 -| Name | Repository | Publisher | -| --------------------- | -------------------------------------------------- | -------------------- | +| Name | Repository | Publisher | +|------|-------------|-----------| | spdx-exceptions@2.5.0 | https://github.com/kemitchell/spdx-exceptions.json | The Linux Foundation | + ### License: Python-2.0 -| Name | Repository | Publisher | -| -------------- | ---------------------------------- | --------- | -| argparse@2.0.1 | https://github.com/nodeca/argparse | N/A | +| Name | Repository | Publisher | +|------|-------------|-----------| +| argparse@2.0.1 | https://github.com/nodeca/argparse | N/A | + + diff --git a/src/controllers/highAvailability.ts b/src/controllers/highAvailability.ts index 20e33de..c5e3325 100644 --- a/src/controllers/highAvailability.ts +++ b/src/controllers/highAvailability.ts @@ -55,7 +55,9 @@ async function acquireLock(): Promise { while (fs.existsSync(lockFilePath)) { if (retryCount >= MAX_RETRIES) { - throw new Error("Failed to acquire lock: maximum retry attempts exceeded"); + throw new Error( + "Failed to acquire lock: maximum retry attempts exceeded", + ); } const backoffMs = BASE_DELAY_MS * Math.pow(2, retryCount); @@ -63,7 +65,9 @@ async function acquireLock(): Promise { const jitter = Math.random() * 0.3 * backoffMs; const delayMs = backoffMs + jitter; - logger.warn(`Lock file exists, waiting ${Math.round(delayMs)}ms before retry ${retryCount + 1}/${MAX_RETRIES}...`); + logger.warn( + `Lock file exists, waiting ${Math.round(delayMs)}ms before retry ${retryCount + 1}/${MAX_RETRIES}...`, + ); await sleep(delayMs); retryCount++; } From 3c348d34cdd5440a71c66b6f3dfce09e1ce1ad16 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 1 Jan 2025 02:10:11 +0100 Subject: [PATCH 093/324] Fix: fixing the logger --- .gitignore | 1 + CREDITS.md | 73 +++++++++++++++++++++----------------------- src/utils/logger.ts | 74 ++++++++++++++++++++++++++++++++++++++------- 3 files changed, 98 insertions(+), 50 deletions(-) diff --git a/.gitignore b/.gitignore index 6c61786..84449de 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ docker .test* # Created by https://www.toptal.com/developers/gitignore/api/node ### Node ### +*-audit.json # Logs logs *.log diff --git a/CREDITS.md b/CREDITS.md index be34b47..050b430 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -4,53 +4,48 @@ This file shows all npm packages used in DockStatAPI (also Dev packages) ### License: (MIT AND CC-BY-3.0) -| Name | Repository | Publisher | -|------|-------------|-----------| +| Name | Repository | Publisher | +| ----------------- | -------------------------------------------- | -------------------- | | spdx-ranges@2.1.1 | https://github.com/kemitchell/spdx-ranges.js | The Linux Foundation | - ### License: Apache-2.0 -| Name | Repository | Publisher | -|------|-------------|-----------| -| @balena/dockerignore@1.0.2 | https://github.com/balena-io-modules/dockerignore | N/A | -| @eslint/config-array@0.19.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/core@0.9.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/object-schema@2.1.5 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/plugin-kit@0.2.4 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @humanfs/core@0.19.1 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | -| @humanfs/node@0.16.6 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | -| @humanwhocodes/module-importer@1.0.1 | https://github.com/humanwhocodes/module-importer | Nicholas C. Zaks | -| @humanwhocodes/retry@0.3.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | -| @humanwhocodes/retry@0.4.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | -| @playwright/test@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | -| @scarf/scarf@1.4.0 | https://github.com/scarf-sh/scarf-js | Scarf Systems | -| detect-libc@2.0.3 | https://github.com/lovell/detect-libc | Lovell Fuller | -| docker-modem@5.0.3 | https://github.com/apocas/docker-modem | Pedro Dias | -| dockerode@4.0.2 | https://github.com/apocas/dockerode | Pedro Dias | -| doctrine@3.0.0 | https://github.com/eslint/doctrine | N/A | -| eslint-visitor-keys@3.4.3 | https://github.com/eslint/eslint-visitor-keys | Toru Nagashima | -| eslint-visitor-keys@4.2.0 | https://github.com/eslint/js | Toru Nagashima | -| playwright-core@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | -| playwright@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | -| spdx-correct@3.2.0 | https://github.com/jslicense/spdx-correct.js | N/A | -| swagger-ui-dist@5.18.2 | https://github.com/swagger-api/swagger-ui | N/A | -| tunnel-agent@0.6.0 | https://github.com/mikeal/tunnel-agent | Mikeal Rogers | -| typescript@5.7.2 | https://github.com/microsoft/TypeScript | Microsoft Corp. | -| validate-npm-package-license@3.0.4 | https://github.com/kemitchell/validate-npm-package-license.js | Kyle E. Mitchell | - +| Name | Repository | Publisher | +| ------------------------------------ | ------------------------------------------------------------- | --------------------- | +| @balena/dockerignore@1.0.2 | https://github.com/balena-io-modules/dockerignore | N/A | +| @eslint/config-array@0.19.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/core@0.9.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/object-schema@2.1.5 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/plugin-kit@0.2.4 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @humanfs/core@0.19.1 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | +| @humanfs/node@0.16.6 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | +| @humanwhocodes/module-importer@1.0.1 | https://github.com/humanwhocodes/module-importer | Nicholas C. Zaks | +| @humanwhocodes/retry@0.3.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | +| @humanwhocodes/retry@0.4.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | +| @playwright/test@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | +| @scarf/scarf@1.4.0 | https://github.com/scarf-sh/scarf-js | Scarf Systems | +| detect-libc@2.0.3 | https://github.com/lovell/detect-libc | Lovell Fuller | +| docker-modem@5.0.3 | https://github.com/apocas/docker-modem | Pedro Dias | +| dockerode@4.0.2 | https://github.com/apocas/dockerode | Pedro Dias | +| doctrine@3.0.0 | https://github.com/eslint/doctrine | N/A | +| eslint-visitor-keys@3.4.3 | https://github.com/eslint/eslint-visitor-keys | Toru Nagashima | +| eslint-visitor-keys@4.2.0 | https://github.com/eslint/js | Toru Nagashima | +| playwright-core@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | +| playwright@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | +| spdx-correct@3.2.0 | https://github.com/jslicense/spdx-correct.js | N/A | +| swagger-ui-dist@5.18.2 | https://github.com/swagger-api/swagger-ui | N/A | +| tunnel-agent@0.6.0 | https://github.com/mikeal/tunnel-agent | Mikeal Rogers | +| typescript@5.7.2 | https://github.com/microsoft/TypeScript | Microsoft Corp. | +| validate-npm-package-license@3.0.4 | https://github.com/kemitchell/validate-npm-package-license.js | Kyle E. Mitchell | ### License: CC-BY-3.0 -| Name | Repository | Publisher | -|------|-------------|-----------| +| Name | Repository | Publisher | +| --------------------- | -------------------------------------------------- | -------------------- | | spdx-exceptions@2.5.0 | https://github.com/kemitchell/spdx-exceptions.json | The Linux Foundation | - ### License: Python-2.0 -| Name | Repository | Publisher | -|------|-------------|-----------| -| argparse@2.0.1 | https://github.com/nodeca/argparse | N/A | - - +| Name | Repository | Publisher | +| -------------- | ---------------------------------- | --------- | +| argparse@2.0.1 | https://github.com/nodeca/argparse | N/A | diff --git a/src/utils/logger.ts b/src/utils/logger.ts index e69955a..8c1ea4a 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -1,18 +1,70 @@ -import winston, { transport } from "winston"; +import { createLogger, format, transports } from "winston"; +import DailyRotateFile from "winston-daily-rotate-file"; import loggerConfig from "../config/loggerConfig"; -const transports: transport[] = [new winston.transports.Console()]; +// ANSI color codes for log level customization +const colors = { + gray: "\x1b[90m", + reset: "\x1b[0m", + white: "\x1b[97m", + red: "\x1b[31m", + green: "\x1b[32m", + yellow: "\x1b[33m", + blue: "\x1b[34m", +}; -transports.push( - new winston.transports.File({ - filename: "./logs/app.log", - }), -); +// Custom formatter to colorize log levels +function colorizeLogLevel(level: string, levelName: string) { + switch (level) { + case "info": + return `${colors.green}${levelName}${colors.reset}`; + case "debug": + return `${colors.blue}${levelName}${colors.reset}`; + case "error": + return `${colors.red}${levelName}${colors.reset}`; + case "warn": + return `${colors.yellow}${levelName}${colors.reset}`; + default: + return `${colors.gray}UNKNOWN${colors.reset}`; + } +} -const logger = winston.createLogger({ - level: loggerConfig.level, - format: loggerConfig.format, - transports, +// Filter out unwanted logs (example: Exit listeners logs) +const filterLogs = format((info) => { + if ( + typeof info.message === "string" && + info.message.includes("Exit listeners detected") + ) { + return false; + } + return info; +}); + +// Logger instance +const logger = createLogger({ + level: loggerConfig.level || "debug", + format: format.combine( + filterLogs(), + format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), + format.printf((info) => { + const level = info.level.toUpperCase().padEnd(5, " "); + const timestamp = `${colors.gray}${info.timestamp}${colors.reset}`; + const levelColorized = colorizeLogLevel(info.level.toLowerCase(), level); + const message = `${colors.white}${info.message}${colors.reset}`; + + return `${timestamp} ${levelColorized} : ${message}`; + }), + ), + transports: [ + new transports.Console(), + new DailyRotateFile({ + filename: "logs/app-%DATE%.log", + datePattern: "YYYY-MM-DD", + maxSize: "20m", + maxFiles: "14d", + zippedArchive: true, + }), + ], }); export default logger; From 59844643d243083ae10cdb7abcb8779a799ac400 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 1 Jan 2025 18:29:56 +0100 Subject: [PATCH 094/324] Chore: Change routes to classes instead of huge functions --- TODO.md | 3 + src/controllers/auth.ts | 64 +++++++++++ src/controllers/containerController.ts | 32 +++--- src/handlers/api.ts | 138 ++++++++++++++++++++++++ src/handlers/auth.ts | 72 +++++++++++++ src/handlers/conf.ts | 97 +++++++++++++++++ src/handlers/data.ts | 93 ++++++++++++++++ src/handlers/frontend.ts | 138 ++++++++++++++++++++++++ src/handlers/ha.ts | 70 ++++++++++++ src/handlers/notification.ts | 73 +++++++++++++ src/handlers/response.ts | 41 +++++++ src/middleware/authMiddleware.ts | 13 ++- src/middleware/checkLock.ts | 10 +- src/routes/auth/routes.ts | 133 +++-------------------- src/routes/data/routes.ts | 89 ++------------- src/routes/frontendController/routes.ts | 115 ++++---------------- src/routes/getter/routes.ts | 125 +++------------------ src/routes/highavailability/routes.ts | 49 ++------- src/routes/notifications/routes.ts | 62 ++--------- src/routes/setter/routes.ts | 115 ++------------------ src/typings/dockerConfig.ts | 1 + src/typings/syncRequestBody.ts | 5 + src/typings/table.ts | 6 +- src/typings/template.ts | 5 + 24 files changed, 920 insertions(+), 629 deletions(-) create mode 100644 src/controllers/auth.ts create mode 100644 src/handlers/api.ts create mode 100644 src/handlers/auth.ts create mode 100644 src/handlers/conf.ts create mode 100644 src/handlers/data.ts create mode 100644 src/handlers/frontend.ts create mode 100644 src/handlers/ha.ts create mode 100644 src/handlers/notification.ts create mode 100644 src/handlers/response.ts create mode 100644 src/typings/syncRequestBody.ts create mode 100644 src/typings/template.ts diff --git a/TODO.md b/TODO.md index 36d3265..fc40ce6 100644 --- a/TODO.md +++ b/TODO.md @@ -11,3 +11,6 @@ - [x] Better /api/status endpoint with connection status of each host - [x] Update notification service - [x] Adjust process.env variables since they don't really work as expected (See [commit](https://github.com/Its4Nik/dockstatapi/pull/21/commits/a03b58c7a17e269f46216df5492e18d008774961)) +- [ ] Better project structure +- [ ] Update logging => Better errors +- [ ] Update json responses and swagger diff --git a/src/controllers/auth.ts b/src/controllers/auth.ts new file mode 100644 index 0000000..905e39c --- /dev/null +++ b/src/controllers/auth.ts @@ -0,0 +1,64 @@ +import fs from "fs/promises"; +import logger from "../utils/logger"; +const passwordFile: string = "./src/data/password.json"; +const passwordBool: string = "./src/data/usePassword.txt"; + +async function authEnabled(): Promise { + let isAuthEnabled: boolean = false; + let data: string = ""; + try { + data = await fs.readFile(passwordBool, "utf8"); + isAuthEnabled = data.trim() === "true"; + return isAuthEnabled; + } catch (error: unknown) { + logger.error("Error reading file: ", error as Error); + return isAuthEnabled; + } +} + +async function readPasswordFile() { + let data: string = ""; + try { + data = await fs.readFile(passwordFile, "utf8"); + return data; + } catch (error: unknown) { + logger.error("Could not read saved password: ", error as Error); + return data; + } +} + +async function writePasswordFile(passwordData: string) { + try { + await fs.writeFile(passwordFile, passwordData); + setTrue(); + logger.debug("Authentication enabled"); + return "Authentication enabled"; + } catch (error: unknown) { + logger.error("Error writing password file:", error as Error); + return error; + } +} + +async function setTrue() { + try { + await fs.writeFile(passwordBool, "true", "utf8"); + logger.info(`Enabled authentication`); + return; + } catch (error: unknown) { + logger.error("Error writing to the file:", error as Error); + return; + } +} + +async function setFalse() { + try { + await fs.writeFile(passwordBool, "false", "utf8"); + logger.info(`Disabled authentication`); + return; + } catch (error: unknown) { + logger.error("Error writing to the file:", error as Error); + return; + } +} + +export { authEnabled, readPasswordFile, writePasswordFile, setFalse }; diff --git a/src/controllers/containerController.ts b/src/controllers/containerController.ts index ef1c8ce..61745e1 100644 --- a/src/controllers/containerController.ts +++ b/src/controllers/containerController.ts @@ -1,22 +1,25 @@ import getDockerClient from "../utils/dockerClient"; import logger from "../utils/logger"; import { Request, Response } from "express"; +import { createResponseHandler } from "../handlers/response"; const getContainers = async (req: Request, res: Response): Promise => { + const ResponseHandler = createResponseHandler(res); const host: string = (req.query.host as string) || "local"; + logger.info(`Fetching containers from host: ${host}`); + try { const docker = getDockerClient(host); const containers = await docker.listContainers(); - res.status(200).json(containers); - } catch (error: unknown) { - logger.error( - `Error fetching containers from host: ${host} - ${(error as Error).message || "Unknown error"} - Full error: ${JSON.stringify(error, null, 2)}`, + return ResponseHandler.rawData( + containers, + `Fetched containers from ${host}`, ); - res.status(500).json({ - error: `Error fetching containers: ${(error as Error).message || "Unknown error"}`, - }); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); } }; @@ -28,21 +31,20 @@ const getContainerStats = async ( logger.info( `Fetching stats for container: ${containerID} from host: ${containerHost}`, ); + const ResponseHandler = createResponseHandler(res); + try { const docker = getDockerClient(containerHost); const container = docker.getContainer(containerID); const stats = await container.stats({ stream: false }); - logger.info( + + return ResponseHandler.rawData( + stats, `Successfully fetched stats for container: ${containerID} from host: ${containerHost}`, ); - res.status(200).json(stats); } catch (error: unknown) { - logger.error( - `Error fetching stats for container: ${containerID} from host: ${containerHost} - ${(error as Error).message}`, - ); - res.status(500).json({ - error: `Error fetching container stats: ${(error as Error).message}`, - }); + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); } }; diff --git a/src/handlers/api.ts b/src/handlers/api.ts new file mode 100644 index 0000000..3752968 --- /dev/null +++ b/src/handlers/api.ts @@ -0,0 +1,138 @@ +import extractRelevantData from "../utils/extractHostData"; +import { Request, Response } from "express"; +import getDockerClient from "../utils/dockerClient"; +import fetchAllContainers from "../utils/containerService"; +import { getCurrentSchedule } from "../controllers/scheduler"; +import fs from "fs"; +import checkReachability from "../utils/connectionChecker"; +const configPath = "./src/data/dockerConfig.json"; +const userConf = "./src/data/user.conf"; +import { dockerConfig } from "../typings/dockerConfig"; +import { createResponseHandler } from "./response"; + +class ApiHandler { + private req: Request; + private res: Response; + + constructor(req: Request, res: Response) { + this.req = req; + this.res = res; + } + + hosts() { + const ResponseHandler = createResponseHandler(this.res); + try { + const rawData = fs.readFileSync(configPath, "utf-8"); + const config: dockerConfig = JSON.parse(rawData); + + if (!config.hosts) { + return ResponseHandler.error("No hosts defined in configuration.", 400); + } + + const hosts = config.hosts.map((host) => host.name); + return ResponseHandler.rawData(hosts, "Fetched data for all hosts"); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + system() { + const ResponseHandler = createResponseHandler(this.res); + try { + const rawData = fs.readFileSync(userConf, "utf8"); + const config = JSON.parse(rawData); + + if (!config) { + return ResponseHandler.error("Received empty configuration", 400); + } + + return ResponseHandler.rawData(config, "Fetched system configuration"); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + async hostStats(hostName: string) { + const ResponseHandler = createResponseHandler(this.res); + try { + const docker = getDockerClient(hostName); + const info = await docker.info(); + const version = await docker.version(); + const relevantData = extractRelevantData({ hostName, info, version }); + + return ResponseHandler.rawData(relevantData, "Fetched Host stats"); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + async containers() { + const ResponseHandler = createResponseHandler(this.res); + try { + const allContainerData = await fetchAllContainers(); + return ResponseHandler.rawData( + allContainerData, + "Fetched all containers across all hosts", + ); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + async config() { + const ResponseHandler = createResponseHandler(this.res); + try { + const rawData = fs.readFileSync(configPath); + const data = JSON.parse(rawData.toString()); + return ResponseHandler.rawData(data, "Fetched config"); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + currentSchedule() { + const ResponseHandler = createResponseHandler(this.res); + try { + const currentSchedule = getCurrentSchedule(); + return ResponseHandler.rawData( + currentSchedule, + "Fetched current schedule", + ); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + async status() { + const ResponseHandler = createResponseHandler(this.res); + try { + const data = await checkReachability(); + return ResponseHandler.rawData(data, "Fetched Status"); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + frontendConfig() { + const configPath: string = "./src/data/frontendConfiguration.json"; + const ResponseHandler = createResponseHandler(this.res); + try { + const rawData = fs.readFileSync(configPath); + const data = JSON.parse(rawData.toString()); + ResponseHandler.rawData(data, "Fetched frontend configuration"); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } +} + +export const createApiHandler = (req: Request, res: Response) => + new ApiHandler(req, res); diff --git a/src/handlers/auth.ts b/src/handlers/auth.ts new file mode 100644 index 0000000..4dfbd3f --- /dev/null +++ b/src/handlers/auth.ts @@ -0,0 +1,72 @@ +import { Request, Response } from "express"; +import { + authEnabled, + readPasswordFile, + writePasswordFile, + setFalse, +} from "../controllers/auth"; +import { createResponseHandler } from "./response"; +import bcrypt from "bcrypt"; + +const saltRounds: number = 10; + +class AuthenticationHandler { + private req: Request; + private res: Response; + + constructor(req: Request, res: Response) { + this.req = req; + this.res = res; + } + + async enable(password: string) { + const ResponseHandler = createResponseHandler(this.res); + try { + if (await authEnabled()) { + return ResponseHandler.denied( + "Password Authentication is already enabled, please deactivate it first", + ); + } + + if (!password) { + return ResponseHandler.denied("Password is required"); + } + + const salt = await bcrypt.genSalt(saltRounds); + const hash = await bcrypt.hash(password, salt); + + const passwordData = { hash, salt }; + writePasswordFile(JSON.stringify(passwordData)); + + return ResponseHandler.ok("Authentication enabled successfully"); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + async disable(password: string) { + const ResponseHandler = createResponseHandler(this.res); + try { + if (!password) { + return ResponseHandler.denied("Password is required"); + } + + const storedData = JSON.parse(await readPasswordFile()); + const isPasswordValid = await bcrypt.compare(password, storedData.hash); + + if (!isPasswordValid) { + return ResponseHandler.error("Invalid password", 401); + } + + await setFalse(); + return ResponseHandler.ok("Authentication disabled successfully"); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } +} + +export const createAuthenticationHandler = (req: Request, res: Response) => + new AuthenticationHandler(req, res); diff --git a/src/handlers/conf.ts b/src/handlers/conf.ts new file mode 100644 index 0000000..e383c4d --- /dev/null +++ b/src/handlers/conf.ts @@ -0,0 +1,97 @@ +import { setFetchInterval, parseInterval } from "../controllers/scheduler"; +import { Request, Response } from "express"; +import fs from "fs"; +import { createResponseHandler } from "./response"; +import { target, dockerConfig } from "../typings/dockerConfig"; +const configPath: string = "./src/data/dockerConfig.json"; + +class ConfHandler { + private req: Request; + private res: Response; + + constructor(req: Request, res: Response) { + this.req = req; + this.res = res; + } + + addHost(req: Request) { + const ResponseHandler = createResponseHandler(this.res); + + try { + const { name, url, port } = req.query as unknown as target; + if (!name || !url || !port) { + return ResponseHandler.denied("Name, Port, and URL are required."); + } + + const config: dockerConfig = JSON.parse( + fs.readFileSync(configPath, "utf-8"), + ); + + if (config.hosts.some((host) => host.name === name)) { + return ResponseHandler.denied("Host already exists."); + } + + config.hosts.push({ name, url, port }); + fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); + + return ResponseHandler.ok("Host added successfully."); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + removeHost(req: Request) { + const ResponseHandler = createResponseHandler(this.res); + try { + const hostName = req.query.hostName as string; + + if (!hostName) { + return ResponseHandler.denied("Host name is required."); + } + + const currentState = fs.readFileSync(configPath, "utf-8"); + const config: dockerConfig = JSON.parse(currentState); + + const hostIndex = config.hosts.findIndex( + (host) => host.name === hostName, + ); + + if (hostIndex === -1) { + return ResponseHandler.error("Host not found.", 404); + } + + config.hosts.splice(hostIndex, 1); + + fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); + + return ResponseHandler.ok("Host removed successfully."); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + scheduler(req: Request) { + const ResponseHandler = createResponseHandler(this.res); + try { + const interval = req.query.interval as string; + const newInterval = parseInterval(interval); + + if (newInterval < 5 * 60 * 1000 || newInterval > 6 * 60 * 60 * 1000) { + return ResponseHandler.denied( + "Interval must be between 5 minutes and 6 hours.", + ); + } + + setFetchInterval(newInterval); + return ResponseHandler.ok("Updated interval"); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } +} + +export const createConfHandler = (req: Request, res: Response) => + new ConfHandler(req, res); diff --git a/src/handlers/data.ts b/src/handlers/data.ts new file mode 100644 index 0000000..fd3515d --- /dev/null +++ b/src/handlers/data.ts @@ -0,0 +1,93 @@ +import { Response, Request } from "express"; +import db from "../config/db"; +import { Table, DataRow } from "../typings/table"; +import { createResponseHandler } from "./response"; + +function formatRows(rows: DataRow[]): Record { + return rows.reduce( + ( + acc: Record, + row, + index: number, + ): Record => { + acc[index] = JSON.parse(row.info); + return acc; + }, + {}, + ); +} + +class DatabaseHandler { + private req: Request; + private res: Response; + + constructor(req: Request, res: Response) { + this.req = req; + this.res = res; + } + + latest() { + const ResponseHandler = createResponseHandler(this.res); + db.get( + "SELECT info FROM data ORDER BY timestamp DESC LIMIT 1", + (error: unknown, row: Partial> | undefined) => { + if (error) { + return ResponseHandler.critical(error as string); + } + + if (!row || !row.info) { + return ResponseHandler.error( + "No data available for /data/latest", + 404, + ); + } + + try { + return ResponseHandler.rawData( + JSON.parse(row.info), + "Read latest data", + ); + } catch (error: unknown) { + const errorMsg = + error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + }, + ); + } + + all() { + const ResponseHandler = createResponseHandler(this.res); + const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); + + db.all( + "SELECT info FROM data WHERE timestamp >= ?", + [oneDayAgo], + (error: unknown, rows: Pick[] | undefined) => { + if (error) { + return ResponseHandler.critical(error as string); + } + + if (!rows || rows.length === 0) { + return ResponseHandler.error("No data available", 404); + } + + return ResponseHandler.rawData(formatRows(rows), "Read database"); + }, + ); + } + + clear() { + const ResponseHandler = createResponseHandler(this.res); + db.run("DELETE FROM data", (error: unknown) => { + if (error) { + return ResponseHandler.critical(error as string); + } + + return ResponseHandler.ok("Database cleared successfully"); + }); + } +} + +export const createDatabaseHandler = (req: Request, res: Response) => + new DatabaseHandler(req, res); diff --git a/src/handlers/frontend.ts b/src/handlers/frontend.ts new file mode 100644 index 0000000..6b2edc5 --- /dev/null +++ b/src/handlers/frontend.ts @@ -0,0 +1,138 @@ +import { Request, Response } from "express"; +import { createResponseHandler } from "./response"; +import { + hideContainer, + unhideContainer, + addTagToContainer, + removeTagFromContainer, + pinContainer, + unpinContainer, + setLink, + removeLink, + setIcon, + removeIcon, +} from "../controllers/frontendConfiguration"; + +class FrontendHandler { + private req: Request; + private res: Response; + + constructor(req: Request, res: Response) { + this.req = req; + this.res = res; + } + + async show(containerName: string) { + const ResponseHandler = createResponseHandler(this.res); + try { + await unhideContainer(containerName); + return ResponseHandler.ok("Container unhidden successfully."); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + async hide(containerName: string) { + const ResponseHandler = createResponseHandler(this.res); + try { + await hideContainer(containerName); + return ResponseHandler.ok("Hid container succesfully"); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + async addTag(containerName: string, tag: string) { + const ResponseHandler = createResponseHandler(this.res); + try { + await addTagToContainer(containerName, tag); + return ResponseHandler.ok("Tag added successfully."); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + async removeTag(containerName: string, tag: string) { + const ResponseHandler = createResponseHandler(this.res); + try { + await removeTagFromContainer(containerName, tag); + ResponseHandler.ok("Tag removed successfully."); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + async pin(containerName: string) { + const ResponseHandler = createResponseHandler(this.res); + try { + await pinContainer(containerName); + return ResponseHandler.ok("Container pinned successfully."); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + async unPin(containerName: string) { + const ResponseHandler = createResponseHandler(this.res); + try { + await unpinContainer(containerName); + return ResponseHandler.ok("Container unpinned successfully."); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + async addLink(containerName: string, link: string) { + const ResponseHandler = createResponseHandler(this.res); + try { + await setLink(containerName, link); + return ResponseHandler.ok("Link added successfully."); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + async removeLink(containerName: string) { + const ResponseHandler = createResponseHandler(this.res); + try { + await removeLink(containerName); + return ResponseHandler.ok("Removed link succesfully"); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + async addIcon(containerName: string, icon: string, useCustomIcon: string) { + const ResponseHandler = createResponseHandler(this.res); + const iconBool = useCustomIcon === "true"; + try { + await setIcon(containerName, icon, iconBool); + return ResponseHandler.ok("Icon added successfully."); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + async removeIcon(containerName: string) { + const ResponseHandler = createResponseHandler(this.res); + try { + await removeIcon(containerName); + return ResponseHandler.ok("Icon removed successfully."); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } +} + +export const createFrontendHandler = (req: Request, res: Response) => + new FrontendHandler(req, res); diff --git a/src/handlers/ha.ts b/src/handlers/ha.ts new file mode 100644 index 0000000..16c9ae1 --- /dev/null +++ b/src/handlers/ha.ts @@ -0,0 +1,70 @@ +import { Request, Response } from "express"; +import logger from "../utils/logger"; +import { + readConfig, + prepareFilesForSync, + ensureFileExists, +} from "../controllers/highAvailability"; +import { createResponseHandler } from "./response"; + +class HaHandler { + private req: Request; + private res: Response; + + constructor(req: Request, res: Response) { + this.req = req; + this.res = res; + } + + async config() { + const ResponseHandler = createResponseHandler(this.res); + try { + const data = await readConfig(); + return ResponseHandler.rawData(data, "Fetched HA-Config"); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + async sync(req: Request) { + const ResponseHandler = createResponseHandler(this.res); + try { + const { files } = req.body; + logger.info("Received synchronization request from master node."); + if (!files || typeof files !== "object") { + return ResponseHandler.error( + "Invalid request: 'files' object is missing or invalid.", + 400, + ); + } + + for (const [filePath, content] of Object.entries(files)) { + await ensureFileExists(filePath, content as string); + } + + return ResponseHandler.ok("Synchronization completed successfully."); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + async prepare() { + const ResponseHandler = createResponseHandler(this.res); + try { + logger.info("Preparing files for synchronization."); + const fileData = await prepareFilesForSync(); + return ResponseHandler.rawData( + fileData, + "Done preparing files for synchronization", + ); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } +} + +export const createHaHandler = (req: Request, res: Response) => + new HaHandler(req, res); diff --git a/src/handlers/notification.ts b/src/handlers/notification.ts new file mode 100644 index 0000000..ad5c293 --- /dev/null +++ b/src/handlers/notification.ts @@ -0,0 +1,73 @@ +import { Request, Response } from "express"; +import fs from "fs"; +import notify from "../utils/notifications/_notify"; +const dataTemplate = "./src/data/template.json"; +import { TemplateData } from "../typings/template"; +import { createResponseHandler } from "./response"; + +function isTemplateData(data: TemplateData): data is TemplateData { + return ( + data !== null && typeof data === "object" && typeof data.text === "string" + ); +} + +class NotificationHandler { + private req: Request; + private res: Response; + + constructor(req: Request, res: Response) { + this.req = req; + this.res = res; + } + + getTemplate() { + const ResponseHandler = createResponseHandler(this.res); + try { + fs.readFile(dataTemplate, "utf-8", (error: unknown, data) => { + if (error) { + return ResponseHandler.error(error as string, 400); + } + return ResponseHandler.rawData(data, "Fetched notification template"); + }); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + setTemplate(req: Request) { + const ResponseHandler = createResponseHandler(this.res); + const newTemplate: TemplateData = req.body; + + try { + if (!isTemplateData(newTemplate)) { + return ResponseHandler.error( + "Invalid input format. Expected JSON with a 'text' field.", + 400, + ); + } + + fs.writeFileSync(dataTemplate, JSON.stringify(newTemplate, null, 2)); + return ResponseHandler.ok("Template updated successfully."); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + async test(req: Request) { + const { type, containerId } = req.params; + const ResponseHandler = createResponseHandler(this.res); + + try { + await notify(type, containerId); + return ResponseHandler.ok("Sent test notification"); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } +} + +export const createNotificationHandler = (req: Request, res: Response) => + new NotificationHandler(req, res); diff --git a/src/handlers/response.ts b/src/handlers/response.ts new file mode 100644 index 0000000..8c6e95b --- /dev/null +++ b/src/handlers/response.ts @@ -0,0 +1,41 @@ +import { Response } from "express"; +import logger from "../utils/logger"; + +class ResponseHandler { + private res: Response; + + constructor(res: Response) { + this.res = res; + } + + rawData(data: unknown, message: string) { + logger.info(message); + this.res.status(200).json(data); + } + + ok(message: string) { + logger.info(message); + this.res.status(200).json({ status: "success", message }); + } + + denied(message: string) { + logger.warn(message); + this.res.status(403).json({ status: "denied", message }); + } + + error(message: string, code: number) { + logger.error(`Code: ${code} - ${message}`); + this.res.status(code).json({ status: "error", message }); + } + + critical(log: string) { + logger.error(log); + this.res.status(500).json({ + status: "critical", + message: "Please see the server logs for more info", + }); + } +} + +export const createResponseHandler = (res: Response) => + new ResponseHandler(res); diff --git a/src/middleware/authMiddleware.ts b/src/middleware/authMiddleware.ts index 500a7fa..4afb393 100644 --- a/src/middleware/authMiddleware.ts +++ b/src/middleware/authMiddleware.ts @@ -2,7 +2,7 @@ import bcrypt from "bcrypt"; import { Request, Response, NextFunction } from "express"; import logger from "../utils/logger"; import { rateLimitedReadFile } from "../utils/rateLimitFS"; - +import { createResponseHandler } from "../handlers/response"; const passwordFile = "./src/data/password.json"; const passwordBool = "./src/data/usePassword.txt"; @@ -11,6 +11,7 @@ async function authMiddleware( res: Response, next: NextFunction, ): Promise { + const ResponseHandler = createResponseHandler(res); try { const authStatusData = await rateLimitedReadFile(passwordBool); const isAuthEnabled = authStatusData.trim() === "true"; @@ -23,8 +24,7 @@ async function authMiddleware( const providedPassword = req.headers["x-password"]; if (!providedPassword) { - logger.error("Password required - Denied"); - res.status(401).json({ message: "Password required" }); + ResponseHandler.denied("Password required"); return; } @@ -36,16 +36,15 @@ async function authMiddleware( storedData.hash, ); if (!passwordMatch) { - logger.error("Invalid Password - Denied access"); - res.status(401).json({ message: "Invalid password" }); + ResponseHandler.denied("Invalid Password"); return; } logger.debug("Authentication succesfull"); next(); } catch (error: unknown) { - logger.error("Error in authMiddleware:", error); - res.status(500).json({ message: "Internal server error" }); + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); } } diff --git a/src/middleware/checkLock.ts b/src/middleware/checkLock.ts index 73740a0..c01540f 100644 --- a/src/middleware/checkLock.ts +++ b/src/middleware/checkLock.ts @@ -1,5 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { rateLimitedExistsSync } from "../utils/rateLimitFS"; +import { createResponseHandler } from "../handlers/response"; const lockFilePath = "./src/data/ha.lock"; @@ -8,11 +9,12 @@ export async function blockWhileLocked( res: Response, next: NextFunction, ): Promise { + const ResponseHandler = createResponseHandler(res); if (await rateLimitedExistsSync(lockFilePath)) { - res.status(503).json({ - error: - "Service unavailable. The high-availability lock is currently active. Please try again later.", - }); + ResponseHandler.error( + "Service unavailable. The high-availability lock is currently active. Please try again later.", + 503, + ); } else { next(); } diff --git a/src/routes/auth/routes.ts b/src/routes/auth/routes.ts index f7e0b18..47ff6f2 100644 --- a/src/routes/auth/routes.ts +++ b/src/routes/auth/routes.ts @@ -1,69 +1,7 @@ import { Router, Request, Response } from "express"; -import bcrypt from "bcrypt"; -import fs from "fs/promises"; -import logger from "../../utils/logger"; -const passwordFile: string = "./src/data/password.json"; -const passwordBool: string = "./src/data/usePassword.txt"; -const saltRounds: number = 10; -const router: Router = Router(); +import { createAuthenticationHandler } from "../../handlers/auth"; -async function authEnabled(): Promise { - let isAuthEnabled: boolean = false; - let data: string = ""; - try { - data = await fs.readFile(passwordBool, "utf8"); - isAuthEnabled = data.trim() === "true"; - return isAuthEnabled; - } catch (error: unknown) { - logger.error("Error reading file: ", error as Error); - return isAuthEnabled; - } -} - -async function readPasswordFile() { - let data: string = ""; - try { - data = await fs.readFile(passwordFile, "utf8"); - return data; - } catch (error: unknown) { - logger.error("Could not read saved password: ", error as Error); - return data; - } -} - -async function writePasswordFile(passwordData: string) { - try { - await fs.writeFile(passwordFile, passwordData); - setTrue(); - logger.debug("Authentication enabled"); - return "Authentication enabled"; - } catch (error: unknown) { - logger.error("Error writing password file:", error as Error); - return error; - } -} - -async function setTrue() { - try { - await fs.writeFile(passwordBool, "true", "utf8"); - logger.info(`Enabled authentication`); - return; - } catch (error: unknown) { - logger.error("Error writing to the file:", error as Error); - return; - } -} - -async function setFalse() { - try { - await fs.writeFile(passwordBool, "false", "utf8"); - logger.info(`Disabled authentication`); - return; - } catch (error: unknown) { - logger.error("Error writing to the file:", error as Error); - return; - } -} +const router = Router(); /** * @swagger @@ -75,6 +13,8 @@ async function setFalse() { * - name: password * in: query * required: true + * schema: + * type: string * responses: * 200: * description: Authentication enabled. @@ -84,39 +24,9 @@ async function setFalse() { * description: Error saving password. */ router.post("/enable", async (req: Request, res: Response): Promise => { - try { - const password = req.query.password as string; - - if (await authEnabled()) { - logger.error( - "Password Authentication is already enabled, please deactivate it first", - ); - res.status(401).json({ - message: - "Password Authentication is already enabled, please deactivate it first", - }); - return; - } - - if (!password) { - logger.error("Password is required"); - res.status(400).json({ message: "Password is required" }); - return; - } - - const salt = await bcrypt.genSalt(saltRounds); - const hash = await bcrypt.hash(password, salt); - - const passwordData = { hash, salt }; - writePasswordFile(JSON.stringify(passwordData)); - - res - .status(200) - .json({ message: "Password Authentication enabled successfully" }); - } catch (error: unknown) { - logger.error(`Error enabling password authentication: ${error as Error}`); - res.status(500).json({ message: "An error occurred" }); - } + const password = req.query.password as string; + const handler = createAuthenticationHandler(req, res); + await handler.enable(password); }); /** @@ -129,6 +39,8 @@ router.post("/enable", async (req: Request, res: Response): Promise => { * - name: password * in: query * required: true + * schema: + * type: string * responses: * 200: * description: Authentication disabled. @@ -140,30 +52,9 @@ router.post("/enable", async (req: Request, res: Response): Promise => { * description: Error disabling authentication. */ router.post("/disable", async (req: Request, res: Response): Promise => { - try { - const password = req.query.password as string; - - if (!password) { - logger.error("Password is required!"); - res.status(400).json({ message: "Password is required" }); - return; - } - - const storedData = JSON.parse(await readPasswordFile()); - - const isPasswordValid = await bcrypt.compare(password, storedData.hash); - if (!isPasswordValid) { - logger.error("Invalid password"); - res.status(401).json({ message: "Invalid password" }); - return; - } - - await setFalse(); // Assuming this is an async function - res.status(200).json({ message: "Authentication disabled" }); - } catch (error: unknown) { - logger.error(`Error disabling authentication: ${error as Error}`); - res.status(500).json({ message: "An error occurred" }); - } + const password = req.query.password as string; + const handler = createAuthenticationHandler(req, res); + await handler.disable(password); }); export default router; diff --git a/src/routes/data/routes.ts b/src/routes/data/routes.ts index 108fafe..92a7f97 100644 --- a/src/routes/data/routes.ts +++ b/src/routes/data/routes.ts @@ -1,26 +1,6 @@ -import express from "express"; +import express, { Request, Response } from "express"; const router = express.Router(); -import db from "../../config/db"; -import logger from "../../utils/logger"; -import Table from "../../typings/table"; - -interface DataRow { - info: string; -} - -function formatRows(rows: DataRow[]): Record { - return rows.reduce( - ( - acc: Record, - row, - index: number, - ): Record => { - acc[index] = JSON.parse(row.info); - return acc; - }, - {}, - ); -} +import { createDatabaseHandler } from "../../handlers/data"; /** * @swagger @@ -90,29 +70,9 @@ function formatRows(rows: DataRow[]): Record { * description: Networking mode for the container * example: "bridge" */ -router.get("/latest", (req, res) => { - db.get( - "SELECT info FROM data ORDER BY timestamp DESC LIMIT 1", - (error: unknown, row: Partial> | undefined) => { - if (error) { - logger.error("Error fetching latest data:", (error as Error).message); - return res.status(500).json({ error: "Internal server error" }); - } - - if (!row || !row.info) { - logger.warn("No data available for /data/latest"); - return res.status(404).json({ error: "No data available" }); - } - - logger.debug("Fetching /data/latest"); - try { - res.json(JSON.parse(row.info)); - } catch (error: unknown) { - logger.error("Error parsing data:", (error as Error).message); - res.status(500).json({ error: "Data format error" }); - } - }, - ); +router.get("/latest", (req: Request, res: Response) => { + const DatabaseHandler = createDatabaseHandler(req, res); + return DatabaseHandler.latest(); }); /** @@ -162,30 +122,9 @@ router.get("/latest", (req, res) => { * type: number * example: 3072 */ -router.get("/all", (req, res) => { - const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); - - db.all( - "SELECT info FROM data WHERE timestamp >= ?", - [oneDayAgo], - (error: unknown, rows: Pick[] | undefined) => { - if (error) { - logger.error( - "Error fetching data from last 24 hours:", - (error as Error).message, - ); - return res.status(500).json({ error: "Internal server error" }); - } - - logger.debug("Fetching /data/time/24h"); - if (!rows || rows.length === 0) { - logger.warn("No data available for /data/time/24h"); - return res.status(404).json({ error: "No data available" }); - } - - res.json(formatRows(rows)); - }, - ); +router.get("/all", (req: Request, res: Response) => { + const DatabaseHandler = createDatabaseHandler(req, res); + return DatabaseHandler.all(); }); /** @@ -207,15 +146,9 @@ router.get("/all", (req, res) => { * description: Success message upon database clearance * example: "Database cleared successfully." */ -router.delete("/clear", (req, res) => { - db.run("DELETE FROM data", (error: unknown) => { - if (error) { - logger.error("Error clearing the database:", (error as Error).message); - return res.status(500).json({ error: "Internal server error" }); - } - logger.debug("Database cleared successfully"); - res.json({ message: "Database cleared successfully" }); - }); +router.delete("/clear", (req: Request, res: Response) => { + const DatabaseHandler = createDatabaseHandler(req, res); + return DatabaseHandler.clear(); }); export default router; diff --git a/src/routes/frontendController/routes.ts b/src/routes/frontendController/routes.ts index 0de95fe..39500c5 100644 --- a/src/routes/frontendController/routes.ts +++ b/src/routes/frontendController/routes.ts @@ -1,25 +1,6 @@ import express from "express"; const router = express.Router(); -import { - hideContainer, - unhideContainer, - addTagToContainer, - removeTagFromContainer, - pinContainer, - unpinContainer, - setLink, - removeLink, - setIcon, - removeIcon, -} from "../../controllers/frontendConfiguration"; - -/* -____ ___ ____ _____ -| _ \ / _ \/ ___|_ _| -| |_) | | | \___ \ | | -| __/| |_| |___) || | -|_| \___/|____/ |_| -*/ +import { createFrontendHandler } from "../../handlers/frontend"; /** * @swagger @@ -62,15 +43,10 @@ ____ ___ ____ _____ * type: string * description: Error message */ -// Unhide a container router.post("/show/:containerName", async (req, res) => { - const { containerName } = req.params; - try { - await unhideContainer(containerName); - res.status(200).json({ message: "Container unhidden successfully." }); - } catch (error: unknown) { - res.status(500).json({ error: (error as Error).message }); - } + const FrontendHandler = createFrontendHandler(req, res); + const containerName = req.params.containerName; + return FrontendHandler.show(containerName); }); /** @@ -120,15 +96,10 @@ router.post("/show/:containerName", async (req, res) => { * type: string * description: Error message */ -// Add a tag to a container router.post("/tag/:containerName/:tag", async (req, res) => { const { containerName, tag } = req.params; - try { - await addTagToContainer(containerName, tag); - res.json({ success: true, message: "Tag added successfully." }); - } catch (error: unknown) { - res.status(500).json({ success: false, error: (error as Error).message }); - } + const FrontendHandler = createFrontendHandler(req, res); + return FrontendHandler.addTag(containerName, tag); }); /** @@ -172,15 +143,10 @@ router.post("/tag/:containerName/:tag", async (req, res) => { * type: string * description: Error message */ -// Pin a container router.post("/pin/:containerName", async (req, res) => { const { containerName } = req.params; - try { - await pinContainer(containerName); - res.json({ success: true, message: "Container pinned successfully." }); - } catch (error: unknown) { - res.status(500).json({ success: false, error: (error as Error).message }); - } + const FrontendHandler = createFrontendHandler(req, res); + return FrontendHandler.pin(containerName); }); /** @@ -230,15 +196,10 @@ router.post("/pin/:containerName", async (req, res) => { * type: string * description: Error message */ -// Add link to container router.post("/add-link/:containerName/:link", async (req, res) => { const { containerName, link } = req.params; - try { - await setLink(containerName, link); - res.json({ success: true, message: "Link added successfully." }); - } catch (error: unknown) { - res.status(500).json({ success: false, error: (error as Error).message }); - } + const FrontendHandler = createFrontendHandler(req, res); + return FrontendHandler.addLink(containerName, link); }); /** @@ -294,19 +255,12 @@ router.post("/add-link/:containerName/:link", async (req, res) => { * type: string * description: Error message */ -// Add Icon to container router.post( "/add-icon/:containerName/:icon/:useCustomIcon", async (req, res) => { const { containerName, icon, useCustomIcon } = req.params; - try { - const custom = useCustomIcon === "true"; - - await setIcon(containerName, icon, custom); - res.json({ success: true, message: "Icon added successfully." }); - } catch (error: unknown) { - res.status(500).json({ success: false, error: (error as Error).message }); - } + const FrontendHandler = createFrontendHandler(req, res); + return FrontendHandler.addIcon(containerName, icon, useCustomIcon); }, ); @@ -362,13 +316,8 @@ router.post( // Hide a container router.delete("/hide/:containerName", async (req, res) => { const { containerName } = req.params; - const target = containerName; - try { - await hideContainer(target); - res.json({ success: true, message: `Container, ${target}, hidden.` }); - } catch (error: unknown) { - res.status(500).json({ success: false, error: (error as Error).message }); - } + const FrontendHandler = createFrontendHandler(req, res); + return FrontendHandler.hide(containerName); }); /** @@ -418,15 +367,10 @@ router.delete("/hide/:containerName", async (req, res) => { * type: string * description: Error message */ -// Remove a tag from a container router.delete("/remove-tag/:containerName/:tag", async (req, res) => { const { containerName, tag } = req.params; - try { - await removeTagFromContainer(containerName, tag); - res.json({ success: true, message: "Tag removed successfully." }); - } catch (error: unknown) { - res.status(500).json({ success: false, error: (error as Error).message }); - } + const FrontendHandler = createFrontendHandler(req, res); + return FrontendHandler.removeTag(containerName, tag); }); /** @@ -470,15 +414,10 @@ router.delete("/remove-tag/:containerName/:tag", async (req, res) => { * type: string * description: Error message */ -// Unpin a container router.delete("/unpin/:containerName", async (req, res) => { const { containerName } = req.params; - try { - await unpinContainer(containerName); - res.json({ success: true, message: "Container unpinned successfully." }); - } catch (error: unknown) { - res.status(500).json({ success: false, error: (error as Error).message }); - } + const FrontendHandler = createFrontendHandler(req, res); + return FrontendHandler.unPin(containerName); }); /** @@ -522,15 +461,10 @@ router.delete("/unpin/:containerName", async (req, res) => { * type: string * description: Error message */ -// Remove link from container router.delete("/remove-link/:containerName", async (req, res) => { const { containerName } = req.params; - try { - await removeLink(containerName); - res.json({ success: true, message: "Link removed successfully." }); - } catch (error: unknown) { - res.status(500).json({ success: false, error: (error as Error).message }); - } + const FrontendHandler = createFrontendHandler(req, res); + return FrontendHandler.removeLink(containerName); }); /** @@ -574,15 +508,10 @@ router.delete("/remove-link/:containerName", async (req, res) => { * type: string * description: Error message */ -// Remove icon from container router.delete("/remove-icon/:containerName", async (req, res) => { const { containerName } = req.params; - try { - await removeIcon(containerName); - res.json({ success: true, message: "Icon removed successfully." }); - } catch (error: unknown) { - res.status(500).json({ success: false, error: (error as Error).message }); - } + const FrontendHandler = createFrontendHandler(req, res); + return FrontendHandler.removeIcon(containerName); }); export default router; diff --git a/src/routes/getter/routes.ts b/src/routes/getter/routes.ts index d278075..0912d48 100644 --- a/src/routes/getter/routes.ts +++ b/src/routes/getter/routes.ts @@ -1,15 +1,6 @@ -import extractRelevantData from "../../utils/extractHostData"; import { Router, Request, Response } from "express"; -import getDockerClient from "../../utils/dockerClient"; -import fetchAllContainers from "../../utils/containerService"; -import { getCurrentSchedule } from "../../controllers/scheduler"; -import logger from "../../utils/logger"; -import fs from "fs"; -import checkReachability from "../../utils/connectionChecker"; -const configPath = "./src/data/dockerConfig.json"; +import { createApiHandler } from "../../handlers/api"; const router = Router(); -const userConf = "./src/data/user.conf"; -import { dockerConfig } from "../../typings/dockerConfig"; /** * @swagger @@ -32,22 +23,8 @@ import { dockerConfig } from "../../typings/dockerConfig"; * example: ["local", "remote1"] */ router.get("/hosts", (req: Request, res: Response) => { - logger.info(`Fetching config: ${configPath}`); - try { - const rawData = fs.readFileSync(configPath, "utf-8"); - const config: dockerConfig = JSON.parse(rawData); - - if (!config.hosts) { - throw new Error("No hosts defined in configuration."); - } - - const hosts = config.hosts.map((host) => host.name); - logger.debug("Fetching all available Docker hosts"); - res.status(200).json({ hosts }); - } catch (error: unknown) { - logger.error("Error fetching hosts: " + (error as Error).message); - res.status(500).json({ error: "Failed to fetch Docker hosts" }); - } + const ApiHandler = createApiHandler(req, res); + return ApiHandler.hosts(); }); /** @@ -76,20 +53,8 @@ router.get("/hosts", (req: Request, res: Response) => { * description: Error message detailing the issue encountered. */ router.get("/system", (req: Request, res: Response) => { - logger.info(`Fetching ${userConf}`); - - try { - const rawData = fs.readFileSync(userConf, "utf8"); - const config = JSON.parse(rawData); - - if (!config) { - res.status(500).json({ error: `Error received empty ${userConf}` }); - } - res.status(200).json(config); - } catch (error: unknown) { - logger.error(`Could not fetch ${userConf}: ${error as Error}`); - res.status(500).json({ error: `Failed to fetch ${userConf}` }); - } + const ApiHandler = createApiHandler(req, res); + return ApiHandler.system(); }); /** @@ -135,22 +100,8 @@ router.get("/system", (req: Request, res: Response) => { */ router.get("/host/:hostName/stats", async (req: Request, res: Response) => { const { hostName } = req.params; - logger.info(`Fetching stats for host: ${hostName}`); - try { - const docker = getDockerClient(hostName); - const info = await docker.info(); - const version = await docker.version(); - const relevantData = extractRelevantData({ hostName, info, version }); - - res.status(200).json(relevantData); - } catch (error: unknown) { - logger.error( - `Error fetching stats for host: ${hostName} - ${(error as Error).message || "Unknown error"}`, - ); - res.status(500).json({ - error: `Error fetching host stats: ${(error as Error).message || "Unknown error"}`, - }); - } + const ApiHandler = createApiHandler(req, res); + return ApiHandler.hostStats(hostName); }); /** @@ -227,15 +178,8 @@ router.get("/host/:hostName/stats", async (req: Request, res: Response) => { * description: Error message detailing the issue encountered. */ router.get("/containers", async (req: Request, res: Response) => { - logger.info("Fetching all containers across all hosts"); - try { - const allContainerData = await fetchAllContainers(); - logger.debug("Fetched /api/containers"); - res.status(200).json(allContainerData); - } catch (error: unknown) { - logger.error(`Error fetching containers: ${(error as Error).message}`); - res.status(500).json({ error: "Failed to fetch containers" }); - } + const ApiHandler = createApiHandler(req, res); + return ApiHandler.containers(); }); /** @@ -264,17 +208,8 @@ router.get("/containers", async (req: Request, res: Response) => { * description: Error message detailing the issue encountered. */ router.get("/config", async (req: Request, res: Response) => { - try { - const rawData = fs.readFileSync(configPath); - const jsonData = JSON.parse(rawData.toString()); - logger.debug("Fetching /api/config"); - res.status(200).json(jsonData); - } catch (error: unknown) { - logger.error( - "Error loading dockerConfig.json: " + (error as Error).message, - ); - res.status(500).json({ error: "Failed to load Docker configuration" }); - } + const ApiHandler = createApiHandler(req, res); + return ApiHandler.config(); }); /** @@ -296,9 +231,8 @@ router.get("/config", async (req: Request, res: Response) => { * description: Current fetch interval in seconds. */ router.get("/current-schedule", (req: Request, res: Response) => { - const currentSchedule = getCurrentSchedule(); - logger.debug("Fetching current shedule"); - res.json(currentSchedule); + const ApiHandler = createApiHandler(req, res); + return ApiHandler.currentSchedule(); }); /** @@ -331,13 +265,8 @@ router.get("/current-schedule", (req: Request, res: Response) => { */ router.get("/status", async (req: Request, res: Response) => { - logger.debug("Fetching /api/status"); - try { - const jsonData = await checkReachability(); - res.status(200).json(jsonData); - } catch (error: unknown) { - logger.error(`Error while fetching data: ${error as Error}`); - } + const ApiHandler = createApiHandler(req, res); + return ApiHandler.status(); }); /** @@ -383,28 +312,8 @@ router.get("/status", async (req: Request, res: Response) => { * description: Error message */ router.get("/frontend-config", (req: Request, res: Response) => { - const configPath: string = "./src/data/frontendConfiguration.json"; - - fs.stat(configPath, (exists) => { - if (exists == null) { - logger.debug(`${configPath} exists, trying to read it`); - } else if (exists.code === "ENOENT") { - logger.warn(`${configPath} doesn't exist, trying to create it`); - fs.promises.writeFile(configPath, JSON.stringify([], null, 2), "utf-8"); - } - }); - - try { - const rawData = fs.readFileSync(configPath); - const jsonData = JSON.parse(rawData.toString()); - - res.status(200).json(jsonData); - } catch (error: unknown) { - logger.error( - "Error loading frontendConfiguration.json: " + (error as Error).message, - ); - res.status(500).json({ error: "Failed to load Frontend configuration" }); - } + const ApiHandler = createApiHandler(req, res); + return ApiHandler.frontendConfig(); }); export default router; diff --git a/src/routes/highavailability/routes.ts b/src/routes/highavailability/routes.ts index 3fadb02..86057bc 100644 --- a/src/routes/highavailability/routes.ts +++ b/src/routes/highavailability/routes.ts @@ -1,16 +1,6 @@ -// File: /src/routes/ha/routes.ts import { Router, Request, Response } from "express"; -import logger from "../../utils/logger"; -import { - readConfig, - prepareFilesForSync, - ensureFileExists, -} from "../../controllers/highAvailability"; - -interface SyncRequestBody { - files: Record; -} - +import { SyncRequestBody } from "../../typings/syncRequestBody"; +import { createHaHandler } from "../../handlers/ha"; const router = Router(); /** @@ -24,9 +14,8 @@ const router = Router(); * description: A JSON object containing the config. */ router.get("/config", async (req: Request, res: Response) => { - logger.info("Getting the HA-Config"); - const data = await readConfig(); - res.status(200).json(data); + const HaHandler = createHaHandler(req, res); + return HaHandler.config(); }); /** @@ -45,29 +34,8 @@ router.post( req: Request<{}, {}, SyncRequestBody>, // eslint-disable-line res: Response, ): Promise => { - try { - const { files } = req.body; - - if (!files || typeof files !== "object") { - const errorMsg = - "Invalid request: 'files' object is missing or invalid."; - logger.error(errorMsg); - res.status(400).json({ message: errorMsg }); - return; - } - - logger.info("Received synchronization request from master node."); - - for (const [filePath, content] of Object.entries(files)) { - await ensureFileExists(filePath, content); - } - - logger.info("Synchronization completed successfully."); - res.status(200).json({ message: "Synchronization completed." }); - } catch (error) { - logger.error(`Error during synchronization: ${(error as Error).message}`); - res.status(500).json({ message: "Synchronization failed." }); - } + const HaHandler = createHaHandler(req, res); + return HaHandler.sync(req); }, ); @@ -82,9 +50,8 @@ router.post( * description: A JSON object containing files to sync. */ router.get("/prepare-sync", async (req: Request, res: Response) => { - logger.info("Preparing files for synchronization."); - const fileData = await prepareFilesForSync(); - res.status(200).json(fileData); + const HaHandler = createHaHandler(req, res); + return HaHandler.prepare(); }); export default router; diff --git a/src/routes/notifications/routes.ts b/src/routes/notifications/routes.ts index 17cf698..4544b8c 100644 --- a/src/routes/notifications/routes.ts +++ b/src/routes/notifications/routes.ts @@ -1,26 +1,7 @@ import { Request, Response, Router } from "express"; -import logger from "../../utils/logger"; -import fs from "fs"; -import notify from "../../utils/notifications/_notify"; -const dataTemplate = "./src/data/template.json"; +import { createNotificationHandler } from "../../handlers/notification"; const router = Router(); -/////////// -// Will be moved! - -interface TemplateData { - text: string; -} - -function isTemplateData(data: TemplateData): data is TemplateData { - return ( - data !== null && typeof data === "object" && typeof data.text === "string" - ); -} - -// Will be moved -/////////// - /** * @swagger * /notification-service/get-template: @@ -53,13 +34,8 @@ function isTemplateData(data: TemplateData): data is TemplateData { * description: Error message */ router.get("/get-template", (req: Request, res: Response) => { - fs.readFile(dataTemplate, "utf-8", (error, data) => { - if (error) { - logger.error("Errored opening:", error); - return res.status(500).json({ message: `Error opening: ${error}` }); - } - res.json(JSON.parse(data)); - }); + const NotificationHandler = createNotificationHandler(req, res); + return NotificationHandler.getTemplate(); }); /** @@ -98,27 +74,8 @@ router.get("/get-template", (req: Request, res: Response) => { * description: Error message */ router.post("/set-template", (req: Request, res: Response): void => { - const newData: TemplateData = req.body; - - if (!isTemplateData(newData)) { - res.status(400).json({ - message: "Invalid input format. Expected JSON with a 'text' field.", - }); - return; - } - - fs.promises - .writeFile(dataTemplate, JSON.stringify(newData, null, 2), "utf-8") - .then(() => { - logger.info("Template updated successfully."); - res.json({ message: "Template updated successfully." }); - }) - .catch((error) => { - logger.error("Error writing to file: " + error.message); - res - .status(500) - .json({ message: `Error writing to file: ${error.message}` }); - }); + const NotificationHandler = createNotificationHandler(req, res); + return NotificationHandler.setTemplate(req); }); /** @@ -165,13 +122,8 @@ router.post("/set-template", (req: Request, res: Response): void => { * type: string */ router.post("/test/:type/:containerId", async (req: Request, res: Response) => { - const { type, containerId } = req.params; - try { - await notify(type, containerId); - res.json({ success: true, message: `Sent test notification to ${type}` }); - } catch (error: unknown) { - res.json({ success: false, message: `Errored: ${error as Error}` }); - } + const NotificationHandler = createNotificationHandler(req, res); + NotificationHandler.test(req); }); export default router; diff --git a/src/routes/setter/routes.ts b/src/routes/setter/routes.ts index 96915a9..75ef747 100644 --- a/src/routes/setter/routes.ts +++ b/src/routes/setter/routes.ts @@ -1,20 +1,6 @@ -import { setFetchInterval, parseInterval } from "../../controllers/scheduler"; -import logger from "../../utils/logger"; import express, { Router, Request, Response } from "express"; -import fs from "fs"; - +import { createConfHandler } from "../../handlers/conf"; const router: Router = express.Router(); -const configPath: string = "./src/data/dockerConfig.json"; - -interface Host { - name: string; - url: string; - port: string; -} - -interface DockerConfig { - hosts: Host[]; -} /** * @swagger @@ -43,46 +29,10 @@ interface DockerConfig { * 500: * description: An error occurred while adding the host. */ - -router.put( - "/addHost", - async ( - req: Request< - unknown, - unknown, - unknown, - { name: string; url: string; port: string } - >, - res: Response, - ): Promise => { - const { name, url, port } = req.query; - - if (!name || !url || !port) { - res.status(400).json({ error: "Name, Port, and URL are required." }); - return; - } - - try { - const config: DockerConfig = JSON.parse( - fs.readFileSync(configPath, "utf-8"), - ); - - if (config.hosts.some((host) => host.name === name)) { - res.status(400).json({ error: "Host already exists." }); - return; - } - - config.hosts.push({ name, url, port }); - fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); - logger.info(`Added new host: ${name}`); - res.status(200).json({ message: "Host added successfully." }); - } catch (error: unknown) { - const err = error as Error; - logger.error("Error adding host: " + err.message); - res.status(500).json({ error: "Failed to add host." }); - } - }, -); +router.put("/addHost", async (req: Request, res: Response): Promise => { + const ConfHandler = createConfHandler(req, res); + return ConfHandler.addHost(req); +}); /** * @swagger @@ -102,24 +52,8 @@ router.put( * description: Invalid interval format or out of range. */ router.put("/scheduler", (req: Request, res: Response) => { - const interval = req.query.interval as string; - - try { - const newInterval = parseInterval(interval); - - if (newInterval < 5 * 60 * 1000 || newInterval > 6 * 60 * 60 * 1000) { - res - .status(400) - .json({ error: "Interval must be between 5 minutes and 6 hours." }); - } - - setFetchInterval(newInterval); - res.status(200).json({ message: `Fetch interval set to ${interval}.` }); - } catch (error: unknown) { - const err = error as Error; - logger.error("Error setting fetch interval: " + err.message); - res.status(400).json({ error: "Invalid interval format." }); - } + const ConfHandler = createConfHandler(req, res); + return ConfHandler.scheduler(req); }); /** @@ -142,39 +76,8 @@ router.put("/scheduler", (req: Request, res: Response) => { * description: An error occurred while removing the host. */ router.delete("/removeHost", (req: Request, res: Response): void => { - const hostName = req.query.hostName as string; - - if (!hostName) { - res.status(400).json({ error: "Host name is required." }); - return; - } - - fs.promises - .readFile(configPath, "utf-8") - .then((rawData) => { - const config: DockerConfig = JSON.parse(rawData); - const hostIndex = config.hosts.findIndex( - (host) => host.name === hostName, - ); - - if (hostIndex === -1) { - res.status(404).json({ error: "Host not found." }); - return; - } - - config.hosts.splice(hostIndex, 1); - - return fs.promises - .writeFile(configPath, JSON.stringify(config, null, 2)) - .then(() => { - logger.info(`Removed host: ${hostName}`); - res.status(200).json({ message: "Host removed successfully." }); - }); - }) - .catch((error) => { - logger.error("Error removing host: " + (error as Error).message); - res.status(500).json({ error: "Failed to remove host." }); - }); + const ConfHandler = createConfHandler(req, res); + return ConfHandler.addHost(req); }); export default router; diff --git a/src/typings/dockerConfig.ts b/src/typings/dockerConfig.ts index fea0f4e..26d7295 100644 --- a/src/typings/dockerConfig.ts +++ b/src/typings/dockerConfig.ts @@ -7,4 +7,5 @@ interface target { interface dockerConfig { hosts: target[]; } + export { dockerConfig, target }; diff --git a/src/typings/syncRequestBody.ts b/src/typings/syncRequestBody.ts new file mode 100644 index 0000000..36fd70a --- /dev/null +++ b/src/typings/syncRequestBody.ts @@ -0,0 +1,5 @@ +interface SyncRequestBody { + files: Record; +} + +export { SyncRequestBody }; diff --git a/src/typings/table.ts b/src/typings/table.ts index 4845eba..cf0c18a 100644 --- a/src/typings/table.ts +++ b/src/typings/table.ts @@ -4,4 +4,8 @@ type Table = { timestamp: string; // ISO 8601 formatted datetime string }; -export default Table; +interface DataRow { + info: string; +} + +export { Table, DataRow }; diff --git a/src/typings/template.ts b/src/typings/template.ts new file mode 100644 index 0000000..71e0c8a --- /dev/null +++ b/src/typings/template.ts @@ -0,0 +1,5 @@ +interface TemplateData { + text: string; +} + +export { TemplateData }; From c5e4e6c14f447fdb392658960a215ee7af8d6d0d Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 1 Jan 2025 19:03:26 +0100 Subject: [PATCH 095/324] Chore: Moving some typings around Fix: Changing to atomic write = no more file system race coditions --- src/config/hostsystem.ts | 3 +- src/config/initFiles.ts | 5 +- src/config/loggerConfig.ts | 63 ------ src/controllers/fetchData.ts | 3 +- src/controllers/highAvailability.ts | 24 +-- src/misc/dependencyGraphs/mermaid-all.txt | 225 +++++++++++++--------- src/typings/atomicWrite.ts | 6 + src/typings/dockerConfig.ts | 26 ++- src/typings/ha.ts | 20 ++ src/typings/hostData.ts | 26 +++ src/typings/response.ts | 6 + src/utils/atomicWrite.ts | 35 ++++ src/utils/connectionChecker.ts | 21 +- src/utils/containerService.ts | 32 +-- src/utils/dockerClient.ts | 17 +- src/utils/extractHostData.ts | 25 +-- src/utils/logger.ts | 3 +- 17 files changed, 273 insertions(+), 267 deletions(-) delete mode 100644 src/config/loggerConfig.ts create mode 100644 src/typings/atomicWrite.ts create mode 100644 src/typings/ha.ts create mode 100644 src/typings/hostData.ts create mode 100644 src/typings/response.ts create mode 100644 src/utils/atomicWrite.ts diff --git a/src/config/hostsystem.ts b/src/config/hostsystem.ts index 91e44ed..e9c04de 100644 --- a/src/config/hostsystem.ts +++ b/src/config/hostsystem.ts @@ -2,6 +2,7 @@ import { RUNNING_IN_DOCKER, VERSION } from "./variables"; import fs from "fs"; import logger from "../utils/logger"; import os from "os"; +import { atomicWrite } from "../utils/atomicWrite"; const userConf = "./src/data/user.conf"; const inDocker: boolean = RUNNING_IN_DOCKER == "true"; @@ -44,7 +45,7 @@ function writeUserConf() { } if (shouldRewriteConfig) { - fs.writeFileSync(userConf, JSON.stringify(installationDetails, null, 2)); + atomicWrite(userConf, JSON.stringify(installationDetails, null, 2)); logger.debug("Configuration file created/updated:", userConf); } diff --git a/src/config/initFiles.ts b/src/config/initFiles.ts index 7982266..008749c 100644 --- a/src/config/initFiles.ts +++ b/src/config/initFiles.ts @@ -1,5 +1,6 @@ -import { writeFileSync, existsSync } from "fs"; +import { existsSync } from "fs"; import logger from "../utils/logger"; +import { atomicWrite } from "../utils/atomicWrite"; const files = [ { @@ -29,7 +30,7 @@ const files = [ function initFiles(): void { files.forEach(({ path: filePath, content }) => { if (!existsSync(filePath)) { - writeFileSync(filePath, content); + atomicWrite(filePath, content); logger.info(`Created: ${filePath}`); } else { logger.debug(`Skipped (already exists): ${filePath}`); diff --git a/src/config/loggerConfig.ts b/src/config/loggerConfig.ts deleted file mode 100644 index f2b30f4..0000000 --- a/src/config/loggerConfig.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { createLogger, format, transports } from "winston"; -import DailyRotateFile from "winston-daily-rotate-file"; - -const gray = "\x1b[90m"; -const reset = "\x1b[0m"; -const white = "\x1b[97m"; -const red = "\x1b[31m"; -const green = "\x1b[32m"; -const yellow = "\x1b[33m"; -const blue = "\x1b[34m"; - -const ignoreExitListenerLogs = format((info) => { - if ( - typeof info.message === "string" && - info.message.includes("Exit listeners detected") - ) { - return false; // Silences annoying logs - } - return info; -}); - -function colorLog(level: string, levelName: string) { - switch (level) { - case "info": - return `${green}${levelName}${reset}`; - case "debug": - return `${blue}${levelName}${reset}`; - case "error": - return `${red}${levelName}${reset}`; - case "warn": - return `${yellow}${levelName}${reset}`; - default: - return `${gray}UNKNOWN${reset}`; - } -} - -const logger = createLogger({ - level: "debug", - format: format.combine( - ignoreExitListenerLogs(), - format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), - format.printf((info) => { - const level = info.level.toUpperCase().padEnd(5, " "); - const timestamp = `${gray}${info.timestamp}${reset}`; - const levelColorized = colorLog(info.level.toLowerCase(), level); - const message = `${white}${info.message}${reset}`; - - return `${timestamp} ${levelColorized} : ${message}`; - }), - ), - transports: [ - new transports.Console(), - new DailyRotateFile({ - filename: "logs/app-%DATE%.log", - datePattern: "YYYY-MM-DD", - maxSize: "20m", - maxFiles: "14d", - zippedArchive: true, - }), - ], -}); - -export default logger; diff --git a/src/controllers/fetchData.ts b/src/controllers/fetchData.ts index dfc2487..07438ec 100644 --- a/src/controllers/fetchData.ts +++ b/src/controllers/fetchData.ts @@ -2,6 +2,7 @@ import db from "../config/db"; import fetchAllContainers from "../utils/containerService"; import logger from "../utils/logger"; import fs from "fs"; +import { atomicWrite } from "../utils/atomicWrite"; const filePath = "./src/data/states.json"; let previousState: { [key: string]: string } = {}; @@ -60,7 +61,7 @@ const fetchData = async (): Promise => { // Compare previous and current state if (JSON.stringify(previousState) !== JSON.stringify(containerStatus)) { - fs.writeFileSync(filePath, JSON.stringify(containerStatus, null, 2)); + atomicWrite(filePath, JSON.stringify(containerStatus, null, 2)); logger.info(`Container states saved to ${filePath}`); // TODO: Add logic + notification levels per service } else { diff --git a/src/controllers/highAvailability.ts b/src/controllers/highAvailability.ts index c5e3325..1b28b4f 100644 --- a/src/controllers/highAvailability.ts +++ b/src/controllers/highAvailability.ts @@ -9,28 +9,11 @@ import { HA_MASTER_IP, HA_NODE, } from "../config/variables"; +import { atomicWrite } from "../utils/atomicWrite"; +import { HighAvailabilityConfig, HaNodeConfig, NodeCache } from "../typings/ha"; const sleep = promisify(setTimeout); -interface HighAvailabilityConfig { - active: boolean; - master: boolean; - nodes: string[]; -} - -interface Node { - ip: string; - id: number; -} - -interface HaNodeConfig { - master: string; -} - -interface NodeCache { - [nodes: string]: Node; -} - const haMasterPath: string = "./src/data/highAvailability.json"; const haNodePath: string = "./src/data/haNode.json"; const nodeCachePath: string = "./src/data/nodeCache.json"; @@ -61,7 +44,6 @@ async function acquireLock(): Promise { } const backoffMs = BASE_DELAY_MS * Math.pow(2, retryCount); - // Add jitter to prevent thundering herd const jitter = Math.random() * 0.3 * backoffMs; const delayMs = backoffMs + jitter; @@ -73,7 +55,7 @@ async function acquireLock(): Promise { } try { - await fs.promises.writeFile(lockFilePath, "locked", { flag: "wx" }); + atomicWrite(lockFilePath, "locked", { exclusive: true }); logger.debug("Lock acquired."); } catch (error) { logger.error(`Error acquiring lock: ${(error as Error).message}`); diff --git a/src/misc/dependencyGraphs/mermaid-all.txt b/src/misc/dependencyGraphs/mermaid-all.txt index e81fdf8..e61282b 100644 --- a/src/misc/dependencyGraphs/mermaid-all.txt +++ b/src/misc/dependencyGraphs/mermaid-all.txt @@ -3,125 +3,160 @@ flowchart LR 0["server.ts"] subgraph 1["controllers"] 2["highAvailability.ts"] -A["proxy.ts"] -B["scheduler.ts"] -D["fetchData.ts"] -T["frontendConfiguration.ts"] +E["proxy.ts"] +F["scheduler.ts"] +H["fetchData.ts"] +V["auth.ts"] +12["frontendConfiguration.ts"] end 3["util"] subgraph 4["config"] 5["variables.ts"] -9["initFiles.ts"] -C["db.ts"] -1F["swaggerConfig.ts"] +D["initFiles.ts"] +G["db.ts"] +1R["swaggerConfig.ts"] end subgraph 6["data"] 7["variables.json"] end -8["init.ts"] -subgraph E["utils"] -F["containerService.ts"] -G["dockerClient.ts"] -J["rateLimitFS.ts"] -W["connectionChecker.ts"] -X["writeOfflineLog.ts"] -subgraph 12["notifications"] -13["_notify.ts"] -14["discord.ts"] -15["_template.ts"] -16["email.ts"] -17["pushbullet.ts"] -18["pushover.ts"] -19["slack.ts"] -1A["telegram.ts"] -1B["whatsapp.ts"] +subgraph 8["typings"] +9["ha.ts"] end -1E["swaggerDocs.ts"] +subgraph A["utils"] +B["atomicWrite.ts"] +I["containerService.ts"] +J["dockerClient.ts"] +O["rateLimitFS.ts"] +16["connectionChecker.ts"] +subgraph 1D["notifications"] +1E["_notify.ts"] +1F["discord.ts"] +1G["_template.ts"] +1H["email.ts"] +1I["pushbullet.ts"] +1J["pushover.ts"] +1K["slack.ts"] +1L["telegram.ts"] +1M["whatsapp.ts"] end -subgraph H["middleware"] -I["authMiddleware.ts"] -K["checkLock.ts"] -L["rateLimiter.ts"] +1Q["swaggerDocs.ts"] end -subgraph M["routes"] -subgraph N["auth"] -O["routes.ts"] +C["init.ts"] +subgraph K["middleware"] +L["authMiddleware.ts"] +P["checkLock.ts"] +Q["rateLimiter.ts"] end -subgraph P["data"] -Q["routes.ts"] +subgraph M["handlers"] +N["response.ts"] +U["auth.ts"] +Y["data.ts"] +11["frontend.ts"] +15["api.ts"] +19["ha.ts"] +1C["notification.ts"] +1P["conf.ts"] end -subgraph R["frontendController"] -S["routes.ts"] +subgraph R["routes"] +subgraph S["auth"] +T["routes.ts"] end -subgraph U["getter"] -V["routes.ts"] +subgraph W["data"] +X["routes.ts"] end -subgraph Y["highavailability"] -Z["routes.ts"] +subgraph Z["frontendController"] +10["routes.ts"] end -subgraph 10["notifications"] -11["routes.ts"] +subgraph 13["getter"] +14["routes.ts"] end -subgraph 1C["setter"] -1D["routes.ts"] +subgraph 17["highavailability"] +18["routes.ts"] +end +subgraph 1A["notifications"] +1B["routes.ts"] +end +subgraph 1N["setter"] +1O["routes.ts"] end end 0-->2 -0-->8 +0-->C 2-->5 +2-->9 +2-->B 2-->3 5-->7 -8-->9 -8-->A -8-->B -8-->I -8-->K -8-->L -8-->O -8-->Q -8-->S -8-->V -8-->Z -8-->11 -8-->1D -8-->1E -A-->5 -B-->C -B-->D -D-->C -D-->F +C-->D +C-->E +C-->F +C-->L +C-->P +C-->Q +C-->T +C-->X +C-->10 +C-->14 +C-->18 +C-->1B +C-->1O +C-->1Q +D-->B +E-->5 F-->G +F-->H +H-->G +H-->B +H-->I +I-->B I-->J -K-->J -Q-->C -S-->T -V-->B -V-->W -V-->F -V-->G -V-->X -Z-->2 -11-->13 -13-->14 -13-->16 -13-->17 -13-->18 -13-->19 -13-->1A -13-->1B -14-->5 +L-->N +L-->O +P-->N +P-->O +T-->U +U-->V +U-->N +X-->Y +Y-->G +Y-->N +10-->11 +11-->12 +11-->N 14-->15 -16-->5 -16-->15 -17-->5 -17-->15 -18-->5 -18-->15 -19-->5 -19-->15 -1A-->5 -1A-->15 -1B-->5 -1B-->15 -1D-->B +15-->F +15-->16 +15-->I +15-->J +15-->N +18-->19 +19-->2 +19-->N +1B-->1C +1C-->1E +1C-->N 1E-->1F +1E-->1H +1E-->1I +1E-->1J +1E-->1K +1E-->1L +1E-->1M +1F-->5 +1F-->1G +1H-->5 +1H-->1G +1I-->5 +1I-->1G +1J-->5 +1J-->1G +1K-->5 +1K-->1G +1L-->5 +1L-->1G +1M-->5 +1M-->1G +1O-->1P +1P-->F +1P-->N +1Q-->1R diff --git a/src/typings/atomicWrite.ts b/src/typings/atomicWrite.ts new file mode 100644 index 0000000..1f4bfb4 --- /dev/null +++ b/src/typings/atomicWrite.ts @@ -0,0 +1,6 @@ +interface AtomicWriteOptions { + mode?: number; + exclusive?: boolean; +} + +export { AtomicWriteOptions }; diff --git a/src/typings/dockerConfig.ts b/src/typings/dockerConfig.ts index 26d7295..a1749d1 100644 --- a/src/typings/dockerConfig.ts +++ b/src/typings/dockerConfig.ts @@ -8,4 +8,28 @@ interface dockerConfig { hosts: target[]; } -export { dockerConfig, target }; +interface HostConfig { + name: string; + [key: string]: string | number; +} + +interface ContainerData { + name: string; + id: string; + hostName: string; + state: string; + cpu_usage: number; + mem_usage: number; + mem_limit: number; + net_rx: number; + net_tx: number; + current_net_rx: number; + current_net_tx: number; + networkMode: string; +} + +interface AllContainerData { + [hostName: string]: ContainerData[] | { error: string }; +} + +export { dockerConfig, target, ContainerData, AllContainerData, HostConfig }; diff --git a/src/typings/ha.ts b/src/typings/ha.ts new file mode 100644 index 0000000..a722fff --- /dev/null +++ b/src/typings/ha.ts @@ -0,0 +1,20 @@ +interface HighAvailabilityConfig { + active: boolean; + master: boolean; + nodes: string[]; +} + +interface Node { + ip: string; + id: number; +} + +interface HaNodeConfig { + master: string; +} + +interface NodeCache { + [nodes: string]: Node; +} + +export { HighAvailabilityConfig, Node, HaNodeConfig, NodeCache }; diff --git a/src/typings/hostData.ts b/src/typings/hostData.ts new file mode 100644 index 0000000..cf5a78d --- /dev/null +++ b/src/typings/hostData.ts @@ -0,0 +1,26 @@ +interface Component { + Name: string; + Version: string; +} + +interface JsonData { + hostName: string; + info: { + ID: string; + Containers: number; + ContainersRunning: number; + ContainersPaused: number; + ContainersStopped: number; + Images: number; + OperatingSystem: string; + KernelVersion: string; + Architecture: string; + MemTotal: number; + NCPU: number; + }; + version: { + Components: Component[]; + }; +} + +export { JsonData }; diff --git a/src/typings/response.ts b/src/typings/response.ts new file mode 100644 index 0000000..b122dfe --- /dev/null +++ b/src/typings/response.ts @@ -0,0 +1,6 @@ +interface StatusResponse { + ApiReachable: boolean; + online: { [key: string]: boolean }; +} + +export { StatusResponse }; diff --git a/src/utils/atomicWrite.ts b/src/utils/atomicWrite.ts new file mode 100644 index 0000000..51f3375 --- /dev/null +++ b/src/utils/atomicWrite.ts @@ -0,0 +1,35 @@ +import fs from "fs"; +import logger from "./logger"; +import { AtomicWriteOptions } from "../typings/atomicWrite"; + +export function atomicWrite( + targetPath: string, + data: string | Buffer | Record, + options: AtomicWriteOptions = {}, +): void { + const { mode = 0o600, exclusive = false } = options; + const tempFile = `${targetPath}.tmp`; + + try { + const writeData = + typeof data === "object" && !(data instanceof Buffer) + ? JSON.stringify(data, null, 2) + : data; + + if (exclusive && fs.existsSync(targetPath)) { + throw new Error(`File already exists: ${targetPath}`); + } + + fs.writeFileSync(tempFile, writeData, { mode }); + + fs.renameSync(tempFile, targetPath); + + logger.debug(`File successfully written to: ${targetPath}`); + } catch (error: unknown) { + if (fs.existsSync(tempFile)) fs.unlinkSync(tempFile); + logger.error( + `Failed to write file at ${targetPath}: ${(error as Error).message}`, + ); + throw error; + } +} diff --git a/src/utils/connectionChecker.ts b/src/utils/connectionChecker.ts index 92efba3..85b00dd 100644 --- a/src/utils/connectionChecker.ts +++ b/src/utils/connectionChecker.ts @@ -1,26 +1,17 @@ import * as fs from "fs"; import * as net from "net"; -import logger from "../config/loggerConfig"; +import logger from "./logger"; +import { target } from "../typings/dockerConfig"; +import { StatusResponse } from "../typings/response"; const filePath: string = "./src/data/dockerConfig.json"; -interface Host { - name: string; - url: string; - port: string; -} - -interface StatusResponse { - ApiReachable: boolean; - online: { [key: string]: boolean }; -} - -async function checkHostStatus(hosts: Host[]): Promise { +async function checkHostStatus(hosts: target[]): Promise { const results: { [key: string]: boolean } = {}; for (const host of hosts) { const { name, url, port } = host; - const isOnline = await checkPort(url, parseInt(port, 10)); + const isOnline = await checkPort(url, port); results[name] = !!isOnline; @@ -65,7 +56,7 @@ async function checkReachability(): Promise { try { const data = fs.readFileSync(filePath, "utf-8"); const parsedData = JSON.parse(data); - const hosts: Host[] = parsedData.hosts; + const hosts: target[] = parsedData.hosts; return await checkHostStatus(hosts); } catch (error: unknown) { logger.error(`Error reading file: ${error as Error}`); diff --git a/src/utils/containerService.ts b/src/utils/containerService.ts index 841e9c2..f9277c1 100644 --- a/src/utils/containerService.ts +++ b/src/utils/containerService.ts @@ -2,31 +2,9 @@ import logger from "./logger"; import { ContainerInfo, ContainerStats, ContainerInspectInfo } from "dockerode"; import getDockerClient from "./dockerClient"; import fs from "fs"; +import { atomicWrite } from "./atomicWrite"; const configPath = "./src/data/dockerConfig.json"; - -interface HostConfig { - name: string; - [key: string]: string | number; -} - -interface ContainerData { - name: string; - id: string; - hostName: string; - state: string; - cpu_usage: number; - mem_usage: number; - mem_limit: number; - net_rx: number; - net_tx: number; - current_net_rx: number; - current_net_tx: number; - networkMode: string; -} - -interface AllContainerData { - [hostName: string]: ContainerData[] | { error: string }; -} +import { AllContainerData, HostConfig } from "../typings/dockerConfig"; function loadConfig() { try { @@ -34,11 +12,7 @@ function loadConfig() { logger.warn( `Config file not found. Creating an empty file at ${configPath}`, ); - fs.writeFileSync( - configPath, - JSON.stringify({ hosts: [] }, null, 2), - "utf-8", - ); + atomicWrite(configPath, JSON.stringify({ hosts: [] }, null, 2)); } const configData = fs.readFileSync(configPath, "utf-8"); diff --git a/src/utils/dockerClient.ts b/src/utils/dockerClient.ts index dc0f5e9..8f2718b 100644 --- a/src/utils/dockerClient.ts +++ b/src/utils/dockerClient.ts @@ -1,24 +1,15 @@ -// src/utils/dockerClient.ts import Docker from "dockerode"; import fs from "fs"; import logger from "./logger"; -interface DockerHostConfig { - name: string; - url: string; - port?: number; -} - -interface DockerConfig { - hosts: DockerHostConfig[]; -} +import { dockerConfig, target } from "../typings/dockerConfig"; -function loadDockerConfig(): DockerConfig { +function loadDockerConfig(): dockerConfig { const configPath = "./src/data/dockerConfig.json"; try { const rawData = fs.readFileSync(configPath, "utf-8"); logger.debug("Refreshed DockerConfig.json"); - return JSON.parse(rawData) as DockerConfig; + return JSON.parse(rawData) as dockerConfig; } catch (error: unknown) { logger.error( "Error loading dockerConfig.json: " + (error as Error).message, @@ -27,7 +18,7 @@ function loadDockerConfig(): DockerConfig { } } -function createDockerClient(hostConfig: DockerHostConfig): Docker { +function createDockerClient(hostConfig: target): Docker { logger.info( `Creating Docker client for host: ${hostConfig.url} on port: ${hostConfig.port || 2375}`, ); diff --git a/src/utils/extractHostData.ts b/src/utils/extractHostData.ts index 25ea016..0af612e 100644 --- a/src/utils/extractHostData.ts +++ b/src/utils/extractHostData.ts @@ -1,27 +1,4 @@ -interface Component { - Name: string; - Version: string; -} - -interface JsonData { - hostName: string; - info: { - ID: string; - Containers: number; - ContainersRunning: number; - ContainersPaused: number; - ContainersStopped: number; - Images: number; - OperatingSystem: string; - KernelVersion: string; - Architecture: string; - MemTotal: number; - NCPU: number; - }; - version: { - Components: Component[]; - }; -} +import { JsonData } from "../typings/hostData"; type ComponentMap = Record; diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 8c1ea4a..d1a3e85 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -1,6 +1,5 @@ import { createLogger, format, transports } from "winston"; import DailyRotateFile from "winston-daily-rotate-file"; -import loggerConfig from "../config/loggerConfig"; // ANSI color codes for log level customization const colors = { @@ -42,7 +41,7 @@ const filterLogs = format((info) => { // Logger instance const logger = createLogger({ - level: loggerConfig.level || "debug", + level: "debug", format: format.combine( filterLogs(), format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), From 604a6cf4b9c765190847fc8b6e512266d2c0454d Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 1 Jan 2025 20:53:29 +0100 Subject: [PATCH 096/324] Chore: Move files around --- README.md | 13 +- package.json | 4 +- .../dependencyGraphs}/.dependency-cruiser.cjs | 0 .../dependencyGraphs/createDependencyGraph.sh | 41 ++++ src/misc/dependencyGraphs/mermaid-all.txt | 217 +++++++----------- src/misc/dependencyGraphs/mermaid-api.txt | 44 ++-- src/misc/dependencyGraphs/mermaid-auth.txt | 21 +- src/misc/dependencyGraphs/mermaid-conf.txt | 36 +-- src/misc/dependencyGraphs/mermaid-data.txt | 22 +- .../dependencyGraphs/mermaid-frontend.txt | 22 +- src/misc/dependencyGraphs/mermaid-ha.txt | 34 ++- .../mermaid-notificationService.txt | 38 +-- src/{utils => misc}/removeUnusedDeps.sh | 0 src/utils/createDependencyGraph.sh | 38 --- 14 files changed, 253 insertions(+), 277 deletions(-) rename src/{ => misc/dependencyGraphs}/.dependency-cruiser.cjs (100%) create mode 100755 src/misc/dependencyGraphs/createDependencyGraph.sh rename src/{utils => misc}/removeUnusedDeps.sh (100%) delete mode 100755 src/utils/createDependencyGraph.sh diff --git a/README.md b/README.md index 4e6daf3..f8e330c 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,15 @@ ![Dockstat Logo](.github/DockStat.png) -_Pipelines:_
-[![Docker Image CI](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-image.yml/badge.svg?branch=main)](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-image.yml)
-[![Build dockstatapi:nightly](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-dev.yaml/badge.svg?branch=dev)](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-dev.yaml)
-[![Tests](https://github.com/Its4Nik/dockstatapi/actions/workflows/validation.yml/badge.svg?branch=dev)](https://github.com/Its4Nik/dockstatapi/actions/workflows/validation.yml) +

+ +# Pipelines + +[![Docker Image CI](https://img.shields.io/github/actions/workflow/status/Its4Nik/dockstatapi/build-image.yml?branch=main&label=Docker%20Image%20CI&style=for-the-badge&logo=docker)](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-image.yml) +[![Build dockstatapi:nightly](https://img.shields.io/github/actions/workflow/status/Its4Nik/dockstatapi/build-dev.yaml?branch=dev&label=Nightly%20Build&style=for-the-badge&logo=github)](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-dev.yaml) +[![Validate](https://img.shields.io/github/actions/workflow/status/Its4Nik/dockstatapi/validation.yml?branch=dev&label=Validation&style=for-the-badge&logo=checkmarx)](https://github.com/Its4Nik/dockstatapi/actions/workflows/validation.yml) + +
This specific branch contains the currently WIP **DockStatAPI-v2**, this update will bring major breaking changes so please be careful. With this new release a couple of extra features (compared to v1) are going to be available. diff --git a/package.json b/package.json index 6dd43ab..29e307e 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,8 @@ "start:build": "npx tsc && node dist/server.js", "dev": "npm run local-env-file && nodemon", "dev:trace": "npm run local-env-file && nodemon --trace-uncaught --trace-warnings", - "dep": "bash ./src/utils/createDependencyGraph.sh", - "dep:remove": "bash ./src/utils/removeUnusedDeps.sh && npm run dep", + "dep": "bash ./src/misc/dependencyGraphs/createDependencyGraph.sh", + "dep:remove": "bash ./src/misc/removeUnusedDeps.sh && npm run dep", "build": "npx tsc", "build:mini": "npx tsc && bash ./src/misc/minifyDist.sh --build-only", "mini": "bash ./src/misc/minifyDist.sh", diff --git a/src/.dependency-cruiser.cjs b/src/misc/dependencyGraphs/.dependency-cruiser.cjs similarity index 100% rename from src/.dependency-cruiser.cjs rename to src/misc/dependencyGraphs/.dependency-cruiser.cjs diff --git a/src/misc/dependencyGraphs/createDependencyGraph.sh b/src/misc/dependencyGraphs/createDependencyGraph.sh new file mode 100755 index 0000000..4e11819 --- /dev/null +++ b/src/misc/dependencyGraphs/createDependencyGraph.sh @@ -0,0 +1,41 @@ +#!/bin/bash +TMP=$(mktemp) +IGNORE="node_modules|logger|.dependency-cruiser|path|fs|os|https|net|process|util" + +cat ./src/init.ts | grep "./routes" | awk '{print $2,$4}' > $TMP + +spawn_worker(){ + local line="$1" + local target_route="$(echo "$line" | cut -d '"' -f2 | sed 's|^./routes|./src/routes|').ts" + local route=$(echo "$line" | awk '{print $1}') + + echo -e "\nRoute: $route \n${target_route}" + + npx depcruise \ + -c ./src/misc/dependencyGraphs/.dependency-cruiser.cjs \ + -p cli-feedback \ + -T mermaid \ + -x "$IGNORE" \ + -f ./src/misc/dependencyGraphs/mermaid-${route}.txt \ + ${target_route} || exit 1 +} + +while read line; do + spawn_worker "$line" & +done < <(cat $TMP) + +npx depcruise \ + -c ./src/misc/dependencyGraphs/.dependency-cruiser.cjs \ + -p cli-feedback \ + -T mermaid \ + -x "$IGNORE" \ + -f ./src/misc/dependencyGraphs/mermaid-all.txt \ + ./src/server.ts || exit 1 + +wait + +find ./src/misc/dependencyGraphs -type f -name "*.txt" -exec sed -i 's/flowchart LR/flowchart TB/g' {} + + +echo -e "\n========\n\n DONE\n\n========" + +exit 0 diff --git a/src/misc/dependencyGraphs/mermaid-all.txt b/src/misc/dependencyGraphs/mermaid-all.txt index e61282b..ad02a82 100644 --- a/src/misc/dependencyGraphs/mermaid-all.txt +++ b/src/misc/dependencyGraphs/mermaid-all.txt @@ -1,20 +1,19 @@ -flowchart LR +flowchart TB -0["server.ts"] -subgraph 1["controllers"] -2["highAvailability.ts"] -E["proxy.ts"] -F["scheduler.ts"] -H["fetchData.ts"] -V["auth.ts"] -12["frontendConfiguration.ts"] +subgraph 0["src"] +1["server.ts"] +subgraph 2["controllers"] +3["highAvailability.ts"] +C["proxy.ts"] +D["scheduler.ts"] +F["fetchData.ts"] +Q["auth.ts"] +X["frontendConfiguration.ts"] end -3["util"] subgraph 4["config"] 5["variables.ts"] -D["initFiles.ts"] -G["db.ts"] -1R["swaggerConfig.ts"] +B["initFiles.ts"] +E["db.ts"] end subgraph 6["data"] 7["variables.json"] @@ -22,141 +21,87 @@ end subgraph 8["typings"] 9["ha.ts"] end -subgraph A["utils"] -B["atomicWrite.ts"] -I["containerService.ts"] -J["dockerClient.ts"] -O["rateLimitFS.ts"] -16["connectionChecker.ts"] -subgraph 1D["notifications"] -1E["_notify.ts"] -1F["discord.ts"] -1G["_template.ts"] -1H["email.ts"] -1I["pushbullet.ts"] -1J["pushover.ts"] -1K["slack.ts"] -1L["telegram.ts"] -1M["whatsapp.ts"] +A["init.ts"] +subgraph G["middleware"] +H["authMiddleware.ts"] +K["checkLock.ts"] +L["rateLimiter.ts"] end -1Q["swaggerDocs.ts"] +subgraph I["handlers"] +J["response.ts"] +P["auth.ts"] +T["data.ts"] +W["frontend.ts"] +10["api.ts"] +13["ha.ts"] +16["notification.ts"] +19["conf.ts"] end -C["init.ts"] -subgraph K["middleware"] -L["authMiddleware.ts"] -P["checkLock.ts"] -Q["rateLimiter.ts"] +subgraph M["routes"] +subgraph N["auth"] +O["routes.ts"] end -subgraph M["handlers"] -N["response.ts"] -U["auth.ts"] -Y["data.ts"] -11["frontend.ts"] -15["api.ts"] -19["ha.ts"] -1C["notification.ts"] -1P["conf.ts"] +subgraph R["data"] +S["routes.ts"] end -subgraph R["routes"] -subgraph S["auth"] -T["routes.ts"] +subgraph U["frontendController"] +V["routes.ts"] end -subgraph W["data"] -X["routes.ts"] +subgraph Y["getter"] +Z["routes.ts"] end -subgraph Z["frontendController"] -10["routes.ts"] +subgraph 11["highavailability"] +12["routes.ts"] end -subgraph 13["getter"] -14["routes.ts"] +subgraph 14["notifications"] +15["routes.ts"] end -subgraph 17["highavailability"] +subgraph 17["setter"] 18["routes.ts"] end -subgraph 1A["notifications"] -1B["routes.ts"] end -subgraph 1N["setter"] -1O["routes.ts"] end -end -0-->2 -0-->C -2-->5 -2-->9 -2-->B -2-->3 +1-->3 +1-->A +3-->5 +3-->9 5-->7 -C-->D -C-->E -C-->F -C-->L -C-->P -C-->Q -C-->T -C-->X -C-->10 -C-->14 -C-->18 -C-->1B -C-->1O -C-->1Q -D-->B -E-->5 -F-->G -F-->H -H-->G -H-->B -H-->I -I-->B -I-->J -L-->N -L-->O -P-->N -P-->O -T-->U -U-->V -U-->N -X-->Y -Y-->G -Y-->N -10-->11 -11-->12 -11-->N -14-->15 -15-->F +A-->B +A-->C +A-->D +A-->H +A-->K +A-->L +A-->O +A-->S +A-->V +A-->Z +A-->12 +A-->15 +A-->18 +C-->5 +D-->E +D-->F +F-->E +H-->J +K-->J +O-->P +P-->Q +P-->J +S-->T +T-->E +T-->J +V-->W +W-->X +W-->J +Z-->10 +10-->D +10-->J +12-->13 +13-->3 +13-->J 15-->16 -15-->I -15-->J -15-->N +16-->J 18-->19 -19-->2 -19-->N -1B-->1C -1C-->1E -1C-->N -1E-->1F -1E-->1H -1E-->1I -1E-->1J -1E-->1K -1E-->1L -1E-->1M -1F-->5 -1F-->1G -1H-->5 -1H-->1G -1I-->5 -1I-->1G -1J-->5 -1J-->1G -1K-->5 -1K-->1G -1L-->5 -1L-->1G -1M-->5 -1M-->1G -1O-->1P -1P-->F -1P-->N -1Q-->1R +19-->D +19-->J diff --git a/src/misc/dependencyGraphs/mermaid-api.txt b/src/misc/dependencyGraphs/mermaid-api.txt index e7c85cc..3cb4811 100644 --- a/src/misc/dependencyGraphs/mermaid-api.txt +++ b/src/misc/dependencyGraphs/mermaid-api.txt @@ -1,32 +1,26 @@ -flowchart LR +flowchart TB -subgraph 0["routes"] -subgraph 1["getter"] -2["routes.ts"] +subgraph 0["src"] +subgraph 1["routes"] +subgraph 2["getter"] +3["routes.ts"] end end -subgraph 3["controllers"] -4["scheduler.ts"] -7["fetchData.ts"] +subgraph 4["handlers"] +5["api.ts"] +B["response.ts"] end -subgraph 5["config"] -6["db.ts"] +subgraph 6["controllers"] +7["scheduler.ts"] +A["fetchData.ts"] end -subgraph 8["utils"] -9["containerService.ts"] -A["dockerClient.ts"] -B["connectionChecker.ts"] -C["extractHostData.ts"] -D["writeOfflineLog.ts"] +subgraph 8["config"] +9["db.ts"] end -2-->4 -2-->B -2-->9 -2-->A -2-->C -2-->D -4-->6 -4-->7 -7-->6 +end +3-->5 +5-->7 +5-->B 7-->9 -9-->A +7-->A +A-->9 diff --git a/src/misc/dependencyGraphs/mermaid-auth.txt b/src/misc/dependencyGraphs/mermaid-auth.txt index aaeb683..336dded 100644 --- a/src/misc/dependencyGraphs/mermaid-auth.txt +++ b/src/misc/dependencyGraphs/mermaid-auth.txt @@ -1,8 +1,19 @@ -flowchart LR +flowchart TB -subgraph 0["routes"] -subgraph 1["auth"] -2["routes.ts"] +subgraph 0["src"] +subgraph 1["routes"] +subgraph 2["auth"] +3["routes.ts"] end end - +subgraph 4["handlers"] +5["auth.ts"] +8["response.ts"] +end +subgraph 6["controllers"] +7["auth.ts"] +end +end +3-->5 +5-->7 +5-->8 diff --git a/src/misc/dependencyGraphs/mermaid-conf.txt b/src/misc/dependencyGraphs/mermaid-conf.txt index ba9ca66..370dd89 100644 --- a/src/misc/dependencyGraphs/mermaid-conf.txt +++ b/src/misc/dependencyGraphs/mermaid-conf.txt @@ -1,24 +1,26 @@ -flowchart LR +flowchart TB -subgraph 0["routes"] -subgraph 1["setter"] -2["routes.ts"] +subgraph 0["src"] +subgraph 1["routes"] +subgraph 2["setter"] +3["routes.ts"] end end -subgraph 3["controllers"] -4["scheduler.ts"] -7["fetchData.ts"] +subgraph 4["handlers"] +5["conf.ts"] +B["response.ts"] end -subgraph 5["config"] -6["db.ts"] +subgraph 6["controllers"] +7["scheduler.ts"] +A["fetchData.ts"] end -subgraph 8["utils"] -9["containerService.ts"] -A["dockerClient.ts"] +subgraph 8["config"] +9["db.ts"] end -2-->4 -4-->6 -4-->7 -7-->6 +end +3-->5 +5-->7 +5-->B 7-->9 -9-->A +7-->A +A-->9 diff --git a/src/misc/dependencyGraphs/mermaid-data.txt b/src/misc/dependencyGraphs/mermaid-data.txt index 107d46a..4aa6a13 100644 --- a/src/misc/dependencyGraphs/mermaid-data.txt +++ b/src/misc/dependencyGraphs/mermaid-data.txt @@ -1,11 +1,19 @@ -flowchart LR +flowchart TB -subgraph 0["routes"] -subgraph 1["data"] -2["routes.ts"] +subgraph 0["src"] +subgraph 1["routes"] +subgraph 2["data"] +3["routes.ts"] end end -subgraph 3["config"] -4["db.ts"] +subgraph 4["handlers"] +5["data.ts"] +8["response.ts"] end -2-->4 +subgraph 6["config"] +7["db.ts"] +end +end +3-->5 +5-->7 +5-->8 diff --git a/src/misc/dependencyGraphs/mermaid-frontend.txt b/src/misc/dependencyGraphs/mermaid-frontend.txt index 0334005..8dde5ce 100644 --- a/src/misc/dependencyGraphs/mermaid-frontend.txt +++ b/src/misc/dependencyGraphs/mermaid-frontend.txt @@ -1,11 +1,19 @@ -flowchart LR +flowchart TB -subgraph 0["routes"] -subgraph 1["frontendController"] -2["routes.ts"] +subgraph 0["src"] +subgraph 1["routes"] +subgraph 2["frontendController"] +3["routes.ts"] end end -subgraph 3["controllers"] -4["frontendConfiguration.ts"] +subgraph 4["handlers"] +5["frontend.ts"] +8["response.ts"] end -2-->4 +subgraph 6["controllers"] +7["frontendConfiguration.ts"] +end +end +3-->5 +5-->7 +5-->8 diff --git a/src/misc/dependencyGraphs/mermaid-ha.txt b/src/misc/dependencyGraphs/mermaid-ha.txt index ce15605..2c789f6 100644 --- a/src/misc/dependencyGraphs/mermaid-ha.txt +++ b/src/misc/dependencyGraphs/mermaid-ha.txt @@ -1,11 +1,31 @@ -flowchart LR +flowchart TB -subgraph 0["routes"] -subgraph 1["highavailability"] -2["routes.ts"] +subgraph 0["src"] +subgraph 1["routes"] +subgraph 2["highavailability"] +3["routes.ts"] end end -subgraph 3["controllers"] -4["highAvailability.ts"] +subgraph 4["handlers"] +5["ha.ts"] +E["response.ts"] end -2-->4 +subgraph 6["controllers"] +7["highAvailability.ts"] +end +subgraph 8["config"] +9["variables.ts"] +end +subgraph A["data"] +B["variables.json"] +end +subgraph C["typings"] +D["ha.ts"] +end +end +3-->5 +5-->7 +5-->E +7-->9 +7-->D +9-->B diff --git a/src/misc/dependencyGraphs/mermaid-notificationService.txt b/src/misc/dependencyGraphs/mermaid-notificationService.txt index cef6c2c..2bc9731 100644 --- a/src/misc/dependencyGraphs/mermaid-notificationService.txt +++ b/src/misc/dependencyGraphs/mermaid-notificationService.txt @@ -1,35 +1,15 @@ -flowchart LR +flowchart TB -subgraph 0["routes"] -subgraph 1["notifications"] -2["routes.ts"] +subgraph 0["src"] +subgraph 1["routes"] +subgraph 2["notifications"] +3["routes.ts"] end end -subgraph 3["utils"] -subgraph 4["notifications"] -5["_notify.ts"] -6["discord.ts"] -7["_template.ts"] -8["email.ts"] -9["pushbullet.ts"] -A["pushover.ts"] -B["slack.ts"] -C["telegram.ts"] -D["whatsapp.ts"] +subgraph 4["handlers"] +5["notification.ts"] +6["response.ts"] end end -2-->5 +3-->5 5-->6 -5-->8 -5-->9 -5-->A -5-->B -5-->C -5-->D -6-->7 -8-->7 -9-->7 -A-->7 -B-->7 -C-->7 -D-->7 diff --git a/src/utils/removeUnusedDeps.sh b/src/misc/removeUnusedDeps.sh similarity index 100% rename from src/utils/removeUnusedDeps.sh rename to src/misc/removeUnusedDeps.sh diff --git a/src/utils/createDependencyGraph.sh b/src/utils/createDependencyGraph.sh deleted file mode 100755 index 9c220f7..0000000 --- a/src/utils/createDependencyGraph.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash -cd src || exit 1 -TMP=$(mktemp) -IGNORE="../node_modules|logger|.dependency-cruiser|path|fs|os|https|net|process" - -cat ./server.ts | grep "./routes" | awk '{print $2,$4}' > $TMP - -spawn_worker(){ - local line="$1" - local target_route="$(echo "$line" | cut -d '"' -f2).ts" - local route=$(echo "$line" | awk '{print $1}') - - echo -e "\nRoute: $route \n${target_route}" - - npx depcruise \ - -p cli-feedback \ - -T mermaid \ - -x "$IGNORE" \ - -f ./misc/dependencyGraphs/mermaid-${route}.txt \ - ${target_route} || exit 1 -} - -while read line; do - spawn_worker "$line" & -done < <(cat $TMP) - -npx depcruise \ - -p cli-feedback \ - -T mermaid \ - -x "$IGNORE" \ - -f ./misc/dependencyGraphs/mermaid-all.txt \ - ./server.ts || exit 1 - -wait - -echo -e "\n========\n\n DONE\n\n========" - -exit 0 From dcf62b70cf38b757b28a3c8f2af741f00947c470 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 1 Jan 2025 20:54:44 +0100 Subject: [PATCH 097/324] Chore: Update ReadMe --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index f8e330c..8fc800a 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,7 @@ # Pipelines [![Docker Image CI](https://img.shields.io/github/actions/workflow/status/Its4Nik/dockstatapi/build-image.yml?branch=main&label=Docker%20Image%20CI&style=for-the-badge&logo=docker)](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-image.yml) -[![Build dockstatapi:nightly](https://img.shields.io/github/actions/workflow/status/Its4Nik/dockstatapi/build-dev.yaml?branch=dev&label=Nightly%20Build&style=for-the-badge&logo=github)](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-dev.yaml) -[![Validate](https://img.shields.io/github/actions/workflow/status/Its4Nik/dockstatapi/validation.yml?branch=dev&label=Validation&style=for-the-badge&logo=checkmarx)](https://github.com/Its4Nik/dockstatapi/actions/workflows/validation.yml) +[![Validation](https://img.shields.io/github/actions/workflow/status/Its4Nik/dockstatapi/validation.yml?branch=dev&label=Validation&style=for-the-badge&logo=checkmarx)](https://github.com/Its4Nik/dockstatapi/actions/workflows/validation.yml) From 45b0cfd8c58726c35321d4c89aa9a0610450dd64 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 1 Jan 2025 20:58:35 +0100 Subject: [PATCH 098/324] Fix: Remove escape characters from log file --- src/utils/logger.ts | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/utils/logger.ts b/src/utils/logger.ts index d1a3e85..00adbdf 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -45,23 +45,35 @@ const logger = createLogger({ format: format.combine( filterLogs(), format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), - format.printf((info) => { - const level = info.level.toUpperCase().padEnd(5, " "); - const timestamp = `${colors.gray}${info.timestamp}${colors.reset}`; - const levelColorized = colorizeLogLevel(info.level.toLowerCase(), level); - const message = `${colors.white}${info.message}${colors.reset}`; - - return `${timestamp} ${levelColorized} : ${message}`; - }), ), transports: [ - new transports.Console(), + new transports.Console({ + format: format.combine( + format.printf((info) => { + const level = info.level.toUpperCase().padEnd(5, " "); + const timestamp = `${colors.gray}${info.timestamp}${colors.reset}`; + const levelColorized = colorizeLogLevel( + info.level.toLowerCase(), + level, + ); + const message = `${colors.white}${info.message}${colors.reset}`; + + return `${timestamp} ${levelColorized} : ${message}`; + }), + ), + }), new DailyRotateFile({ filename: "logs/app-%DATE%.log", datePattern: "YYYY-MM-DD", maxSize: "20m", maxFiles: "14d", zippedArchive: true, + format: format.combine( + format.printf((info) => { + const level = info.level.toUpperCase().padEnd(5, " "); + return `${info.timestamp} ${level} : ${info.message}`; + }), + ), }), ], }); From ffb45521f031c00e96a804744342cc155af58ae2 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 2 Jan 2025 21:56:01 +0100 Subject: [PATCH 099/324] Chore: Cleaning up Dockerfile(s) --- .github/workflows/build-image.yaml | 2 + .github/workflows/validation.yaml | 16 +- .gitignore | 3 +- Dockerfile | 61 ----- Dockerfile-dev | 61 ----- TODO.md | 5 +- docker/Dockerfile-base | 59 +++++ docker/Dockerfile-dev | 59 +++++ .../docker-compose.yaml | 10 +- package-lock.json | 247 ++++++++++-------- package.json | 8 +- src/config/hostsystem.ts | 24 +- src/controllers/highAvailability.ts | 4 +- src/misc/.tmux.sh | 1 + src/server.ts | 5 - 15 files changed, 306 insertions(+), 259 deletions(-) delete mode 100644 Dockerfile delete mode 100644 Dockerfile-dev create mode 100644 docker/Dockerfile-base create mode 100644 docker/Dockerfile-dev rename docker-compose.yaml => docker/docker-compose.yaml (87%) create mode 100644 src/misc/.tmux.sh diff --git a/.github/workflows/build-image.yaml b/.github/workflows/build-image.yaml index 720bed8..41f18cb 100644 --- a/.github/workflows/build-image.yaml +++ b/.github/workflows/build-image.yaml @@ -39,6 +39,8 @@ jobs: uses: docker/build-push-action@v6 with: platforms: linux/amd64,linux/arm64 + context: . + file: docker/Dockerfile-base push: true tags: ${{ steps.metadata.outputs.tags }} labels: ${{ steps.metadata.outputs.labels }} diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index dfd9330..2226171 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -103,7 +103,7 @@ jobs: - uses: actions/checkout@v4 - name: Build the Container image - run: docker build . --file Dockerfile --tag localbuild/testimage:latest + run: docker build . --file docker/Dockerfile-base --tag localbuild/testimage:latest - name: Run Grype test run: grype -o sarif localbuild/testimage:latest > results.sarif @@ -126,16 +126,6 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Set up Node.js version from .nvmrc - run: | - curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash - export NVM_DIR="$HOME/.nvm" - [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" - nvm install - nvm use - node -v - npm -v - - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -160,6 +150,8 @@ jobs: - name: Build and Push Docker Images uses: docker/build-push-action@v6 with: + context: . + file: docker/Dockerfile-base platforms: linux/amd64,linux/arm64 push: false tags: ${{ steps.metadata.outputs.tags }} @@ -205,6 +197,8 @@ jobs: uses: docker/build-push-action@v6 with: platforms: linux/amd64,linux/arm64, + context: . + file: docker/Dockerfile-dev push: true tags: ${{ steps.metadata.outputs.tags }} labels: ${{ steps.metadata.outputs.labels }} diff --git a/.gitignore b/.gitignore index 84449de..dc93b88 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ # custom paths: src/data/* -docker +docker/master +docker/slave .test* # Created by https://www.toptal.com/developers/gitignore/api/node ### Node ### diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 0e59a45..0000000 --- a/Dockerfile +++ /dev/null @@ -1,61 +0,0 @@ -# Stage 1: Build stage -FROM node:alpine AS builder - -LABEL maintainer="https://github.com/its4nik" -LABEL version="2.0.1" -LABEL description="API for DockStat" -LABEL license="BSD-3-Clause license" -LABEL repository="https://github.com/its4nik/dockstatapi" -LABEL documentation="https://github.com/its4nik/dockstatapi" -LABEL org.opencontainers.image.description="The DockSatAPI is a free and OpenSource backend for gathering container statistics across hosts" -LABEL org.opencontainers.image.licenses="BSD-3-Clause license" -LABEL org.opencontainers.image.source="https://github.com/its4nik/dockstatapi" - -WORKDIR /build -ENV NODE_NO_WARNINGS=1 - -RUN apk add --update --no-cache bash - -COPY tsconfig.json environment.d.ts package*.json tsconfig.json ./ -RUN npm install - -COPY ./src ./src -RUN mv ./src/sample-variable.json ./src/data/variables.json -RUN npm run build:mini - -# Stage 2: main stage -FROM alpine AS main - -RUN apk add --update npm - -WORKDIR /build - -RUN mkdir -p /build/src/data - -COPY package*.json ./ -RUN npm install --omit=dev - -COPY --from=builder /build/dist/* /build/src -COPY --from=builder /build/src/misc/entrypoint.sh /build/entrypoint.sh -COPY --from=builder /build/src/misc/createEnvFile.sh /build/createEnvFile.sh - -RUN node src/config/db.js - -# Stage 3: Production stage -FROM alpine AS production - -WORKDIR /api - -RUN apk add --update --no-cache bash curl nodejs && \ - adduser -h /api -s /bin/bash -D dockstatapi dockstatapi && \ - chown -hR dockstatapi:dockstatapi /api - -USER dockstatapi - -HEALTHCHECK --interval=5m --timeout=3s \ - CMD curl -f http://localhost:9876/api/status || exit 1 - -COPY --chown=dockstatapi:dockstatapi --from=main /build /api - -EXPOSE 9876 -ENTRYPOINT [ "bash", "./entrypoint.sh" ] diff --git a/Dockerfile-dev b/Dockerfile-dev deleted file mode 100644 index ba9c01c..0000000 --- a/Dockerfile-dev +++ /dev/null @@ -1,61 +0,0 @@ -# Stage 1: Build stage -FROM node:alpine AS builder - -LABEL maintainer="https://github.com/its4nik" -LABEL version="2.0.1" -LABEL description="API for DockStat" -LABEL license="BSD-3-Clause license" -LABEL repository="https://github.com/its4nik/dockstatapi" -LABEL documentation="https://github.com/its4nik/dockstatapi" -LABEL org.opencontainers.image.description="The DockSatAPI is a free and OpenSource backend for gathering container statistics across hosts" -LABEL org.opencontainers.image.licenses="BSD-3-Clause license" -LABEL org.opencontainers.image.source="https://github.com/its4nik/dockstatapi" - -WORKDIR /build -ENV NODE_NO_WARNINGS=1 - -RUN apk add --update --no-cache bash - -COPY tsconfig.json environment.d.ts package*.json tsconfig.json ./ -RUN npm install - -COPY ./src ./src -RUN mv ./src/sample-variable.json ./src/data/variables.json -RUN npm run build - -# Stage 2: main stage -FROM alpine AS main - -RUN apk add --update npm - -WORKDIR /build - -RUN mkdir -p /build/src/data - -COPY package*.json ./ -RUN npm install --omit=dev - -COPY --from=builder /build/dist/* /build/src -COPY --from=builder /build/src/misc/entrypoint.sh /build/entrypoint.sh -COPY --from=builder /build/src/misc/createEnvFile.sh /build/createEnvFile.sh - -RUN node src/config/db.js - -# Stage 3: Production stage -FROM alpine AS production - -WORKDIR /api - -RUN apk add --update --no-cache bash curl nodejs && \ - adduser -h /api -s /bin/bash -D dockstatapi dockstatapi && \ - chown -hR dockstatapi:dockstatapi /api - -USER dockstatapi - -HEALTHCHECK --interval=5m --timeout=3s \ - CMD curl -f http://localhost:9876/api/status || exit 1 - -COPY --chown=dockstatapi:dockstatapi --from=main /build /api - -EXPOSE 9876 -ENTRYPOINT [ "bash", "./entrypoint.sh" ] diff --git a/TODO.md b/TODO.md index fc40ce6..7ac3d43 100644 --- a/TODO.md +++ b/TODO.md @@ -12,5 +12,6 @@ - [x] Update notification service - [x] Adjust process.env variables since they don't really work as expected (See [commit](https://github.com/Its4Nik/dockstatapi/pull/21/commits/a03b58c7a17e269f46216df5492e18d008774961)) - [ ] Better project structure -- [ ] Update logging => Better errors -- [ ] Update json responses and swagger +- [x] Update logging => Better errors +- [x] Update json responses +- [ ] Swagger update diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base new file mode 100644 index 0000000..1f9bf30 --- /dev/null +++ b/docker/Dockerfile-base @@ -0,0 +1,59 @@ +# Stage 1: Build stage +FROM node:alpine AS builder + +LABEL maintainer="https://github.com/its4nik" +LABEL version="2.0.1" +LABEL description="API for DockStat" +LABEL license="BSD-3-Clause license" +LABEL repository="https://github.com/its4nik/dockstatapi" +LABEL documentation="https://github.com/its4nik/dockstatapi" +LABEL org.opencontainers.image.description="The DockSatAPI is a free and OpenSource backend for gathering container statistics across hosts" +LABEL org.opencontainers.image.licenses="BSD-3-Clause license" +LABEL org.opencontainers.image.source="https://github.com/its4nik/dockstatapi" + +WORKDIR /app + +ENV NODE_NO_WARNINGS=1 + +RUN apk add --no-cache bash + +COPY tsconfig.json environment.d.ts package*.json ./ + +RUN export npm_config_cache=$(mktemp -d) && \ + npm install --production=false && \ + rm -rf $npm_config_cache /tmp/*.log + +COPY ./src ./src +RUN mv ./src/sample-variable.json ./src/data/variables.json +RUN npm run build:mini + +# Stage 2: Production stage +FROM node:alpine AS production + +WORKDIR /api + +RUN apk add --no-cache bash curl && \ + adduser -h /api -s /bin/bash -D dockstatapi + +HEALTHCHECK --interval=5m --timeout=3s \ + CMD curl -f http://localhost:9876/api/status || exit 1 + +COPY --chown=dockstatapi:dockstatapi --from=builder /app/dist/src /api/src +COPY --chown=dockstatapi:dockstatapi --from=builder /app/package*.json /api/ + +RUN export npm_config_cache=$(mktemp -d) && \ + npm install --omit=dev && \ + rm -rf $npm_config_cache /tmp/*.log + +COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/misc/entrypoint.sh /api/entrypoint.sh +COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/misc/createEnvFile.sh /api/createEnvFile.sh +RUN chmod +x /api/*.sh + +EXPOSE 9876 + +RUN chmod -R 777 /api/src/data /api && \ + chown -R dockstatapi:dockstatapi /api + +STOPSIGNAL 130 +USER dockstatapi +ENTRYPOINT [ "bash", "./entrypoint.sh" ] diff --git a/docker/Dockerfile-dev b/docker/Dockerfile-dev new file mode 100644 index 0000000..58b9f43 --- /dev/null +++ b/docker/Dockerfile-dev @@ -0,0 +1,59 @@ +# Stage 1: Build stage +FROM node:alpine AS builder + +LABEL maintainer="https://github.com/its4nik" +LABEL version="2.0.1" +LABEL description="API for DockStat" +LABEL license="BSD-3-Clause license" +LABEL repository="https://github.com/its4nik/dockstatapi" +LABEL documentation="https://github.com/its4nik/dockstatapi" +LABEL org.opencontainers.image.description="The DockSatAPI is a free and OpenSource backend for gathering container statistics across hosts" +LABEL org.opencontainers.image.licenses="BSD-3-Clause license" +LABEL org.opencontainers.image.source="https://github.com/its4nik/dockstatapi" + +WORKDIR /app + +ENV NODE_NO_WARNINGS=1 + +RUN apk add --no-cache bash + +COPY tsconfig.json environment.d.ts package*.json ./ + +RUN export npm_config_cache=$(mktemp -d) && \ + npm install --production=false && \ + rm -rf $npm_config_cache /tmp/*.log + +COPY ./src ./src +RUN mv ./src/sample-variable.json ./src/data/variables.json +RUN npm run build + +# Stage 2: Production stage +FROM node:alpine AS production + +WORKDIR /api + +RUN apk add --no-cache bash curl && \ + adduser -h /api -s /bin/bash -D dockstatapi + +HEALTHCHECK --interval=5m --timeout=3s \ + CMD curl -f http://localhost:9876/api/status || exit 1 + +COPY --chown=dockstatapi:dockstatapi --from=builder /app/dist/src /api/src +COPY --chown=dockstatapi:dockstatapi --from=builder /app/package*.json /api/ + +RUN export npm_config_cache=$(mktemp -d) && \ + npm install --omit=dev && \ + rm -rf $npm_config_cache /tmp/*.log + +COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/misc/entrypoint.sh /api/entrypoint.sh +COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/misc/createEnvFile.sh /api/createEnvFile.sh +RUN chmod +x /api/*.sh + +EXPOSE 9876 + +RUN chmod -R 777 /api/src/data /api && \ + chown -R dockstatapi:dockstatapi /api + +STOPSIGNAL 130 +USER dockstatapi +ENTRYPOINT [ "bash", "./entrypoint.sh" ] diff --git a/docker-compose.yaml b/docker/docker-compose.yaml similarity index 87% rename from docker-compose.yaml rename to docker/docker-compose.yaml index 4789b71..225c5de 100644 --- a/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -5,14 +5,16 @@ networks: services: master: container_name: master + user: "${UID:-1000}:${GID:-1000}" environment: - NODE_ENV=development - - HA_MASTER=false + - HA_MASTER=true - HA_MASTER_IP=master:9876 - HA_NODE=slave:9876 - HA_UNSAFE=true volumes: - - ./docker/master:/api/src/data + - ./master/data:/api/src/data + - ./master/logs:/api/logs ports: - 9876:9876 image: dockstatapi:local @@ -24,10 +26,12 @@ services: slave: container_name: slave + user: "${UID:-1000}:${GID:-1000}" environment: - NODE_ENV=development volumes: - - ./docker/slave:/api/src/data + - ./slave/data:/api/src/data + - ./slave/logs:/api/logs ports: - 6789:9876 image: dockstatapi:local diff --git a/package-lock.json b/package-lock.json index 1310ab8..8c1ea14 100644 --- a/package-lock.json +++ b/package-lock.json @@ -872,26 +872,6 @@ "node-pre-gyp": "bin/node-pre-gyp" } }, - "node_modules/@mapbox/node-pre-gyp/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -956,6 +936,19 @@ "node": ">=10" } }, + "node_modules/@npmcli/move-file/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@playwright/test": { "version": "1.49.1", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.1.tgz", @@ -1109,9 +1102,9 @@ "license": "MIT" }, "node_modules/@types/express-serve-static-core": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.2.tgz", - "integrity": "sha512-vluaspfvWEtE4vcSDlKRNer52DvOGrB2xv6diXy6UKyKW0lqZiWHGNApSyxOv+8DE5Z27IzVvE7hNkxg7EXIcg==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.3.tgz", + "integrity": "sha512-JEhMNwUJt7bw728CydvYzntD0XJeTmDnvwLlbfbAhE7Tbslm/ax6bdIiUwTgeVlZTsJQPwZwKpAkyDtIjsvx3g==", "dev": true, "license": "MIT", "dependencies": { @@ -1142,9 +1135,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.10.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", - "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", + "version": "22.10.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.3.tgz", + "integrity": "sha512-DifAyw4BkrufCILvD3ucnuN8eydUfc/C1GlyrnI+LK6543w5/L3VeVgf05o3B4fqSXP1dKYLOZsKfutpxPzZrw==", "dev": true, "license": "MIT", "dependencies": { @@ -1209,9 +1202,9 @@ } }, "node_modules/@types/ssh2/node_modules/@types/node": { - "version": "18.19.68", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.68.tgz", - "integrity": "sha512-QGtpFH1vB99ZmTa63K4/FU8twThj4fuVSBkGddTp7uIL/cuoLWIUSL2RcOaigBhfR+hg5pgGkBnkoOxrTVBMKw==", + "version": "18.19.69", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.69.tgz", + "integrity": "sha512-ECPdY1nlaiO/Y6GUnwgtAAhLNaQ53AyIVz+eILxpEo5OvuqE6yWkqWBIb5dU0DqhKQtMeny+FBD3PK6lm7L5xQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1257,17 +1250,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.2.tgz", - "integrity": "sha512-adig4SzPLjeQ0Tm+jvsozSGiCliI2ajeURDGHjZ2llnA+A67HihCQ+a3amtPhUakd1GlwHxSRvzOZktbEvhPPg==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.19.0.tgz", + "integrity": "sha512-NggSaEZCdSrFddbctrVjkVZvFC6KGfKfNK0CU7mNK/iKHGKbzT4Wmgm08dKpcZECBu9f5FypndoMyRHkdqfT1Q==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.18.2", - "@typescript-eslint/type-utils": "8.18.2", - "@typescript-eslint/utils": "8.18.2", - "@typescript-eslint/visitor-keys": "8.18.2", + "@typescript-eslint/scope-manager": "8.19.0", + "@typescript-eslint/type-utils": "8.19.0", + "@typescript-eslint/utils": "8.19.0", + "@typescript-eslint/visitor-keys": "8.19.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1287,16 +1280,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.18.2.tgz", - "integrity": "sha512-y7tcq4StgxQD4mDr9+Jb26dZ+HTZ/SkfqpXSiqeUXZHxOUyjWDKsmwKhJ0/tApR08DgOhrFAoAhyB80/p3ViuA==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.19.0.tgz", + "integrity": "sha512-6M8taKyOETY1TKHp0x8ndycipTVgmp4xtg5QpEZzXxDhNvvHOJi5rLRkLr8SK3jTgD5l4fTlvBiRdfsuWydxBw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.18.2", - "@typescript-eslint/types": "8.18.2", - "@typescript-eslint/typescript-estree": "8.18.2", - "@typescript-eslint/visitor-keys": "8.18.2", + "@typescript-eslint/scope-manager": "8.19.0", + "@typescript-eslint/types": "8.19.0", + "@typescript-eslint/typescript-estree": "8.19.0", + "@typescript-eslint/visitor-keys": "8.19.0", "debug": "^4.3.4" }, "engines": { @@ -1312,14 +1305,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.18.2.tgz", - "integrity": "sha512-YJFSfbd0CJjy14r/EvWapYgV4R5CHzptssoag2M7y3Ra7XNta6GPAJPPP5KGB9j14viYXyrzRO5GkX7CRfo8/g==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.19.0.tgz", + "integrity": "sha512-hkoJiKQS3GQ13TSMEiuNmSCvhz7ujyqD1x3ShbaETATHrck+9RaDdUbt+osXaUuns9OFwrDTTrjtwsU8gJyyRA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.18.2", - "@typescript-eslint/visitor-keys": "8.18.2" + "@typescript-eslint/types": "8.19.0", + "@typescript-eslint/visitor-keys": "8.19.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1330,14 +1323,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.18.2.tgz", - "integrity": "sha512-AB/Wr1Lz31bzHfGm/jgbFR0VB0SML/hd2P1yxzKDM48YmP7vbyJNHRExUE/wZsQj2wUCvbWH8poNHFuxLqCTnA==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.19.0.tgz", + "integrity": "sha512-TZs0I0OSbd5Aza4qAMpp1cdCYVnER94IziudE3JU328YUHgWu9gwiwhag+fuLeJ2LkWLXI+F/182TbG+JaBdTg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.18.2", - "@typescript-eslint/utils": "8.18.2", + "@typescript-eslint/typescript-estree": "8.19.0", + "@typescript-eslint/utils": "8.19.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -1354,9 +1347,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.18.2.tgz", - "integrity": "sha512-Z/zblEPp8cIvmEn6+tPDIHUbRu/0z5lqZ+NvolL5SvXWT5rQy7+Nch83M0++XzO0XrWRFWECgOAyE8bsJTl1GQ==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.19.0.tgz", + "integrity": "sha512-8XQ4Ss7G9WX8oaYvD4OOLCjIQYgRQxO+qCiR2V2s2GxI9AUpo7riNwo6jDhKtTcaJjT8PY54j2Yb33kWtSJsmA==", "dev": true, "license": "MIT", "engines": { @@ -1368,14 +1361,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.2.tgz", - "integrity": "sha512-WXAVt595HjpmlfH4crSdM/1bcsqh+1weFRWIa9XMTx/XHZ9TCKMcr725tLYqWOgzKdeDrqVHxFotrvWcEsk2Tg==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.19.0.tgz", + "integrity": "sha512-WW9PpDaLIFW9LCbucMSdYUuGeFUz1OkWYS/5fwZwTA+l2RwlWFdJvReQqMUMBw4yJWJOfqd7An9uwut2Oj8sLw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.18.2", - "@typescript-eslint/visitor-keys": "8.18.2", + "@typescript-eslint/types": "8.19.0", + "@typescript-eslint/visitor-keys": "8.19.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1395,16 +1388,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.18.2.tgz", - "integrity": "sha512-Cr4A0H7DtVIPkauj4sTSXVl+VBWewE9/o40KcF3TV9aqDEOWoXF3/+oRXNby3DYzZeCATvbdksYsGZzplwnK/Q==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.19.0.tgz", + "integrity": "sha512-PTBG+0oEMPH9jCZlfg07LCB2nYI0I317yyvXGfxnvGvw4SHIOuRnQ3kadyyXY6tGdChusIHIbM5zfIbp4M6tCg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.18.2", - "@typescript-eslint/types": "8.18.2", - "@typescript-eslint/typescript-estree": "8.18.2" + "@typescript-eslint/scope-manager": "8.19.0", + "@typescript-eslint/types": "8.19.0", + "@typescript-eslint/typescript-estree": "8.19.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1419,13 +1412,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.2.tgz", - "integrity": "sha512-zORcwn4C3trOWiCqFQP1x6G3xTRyZ1LYydnj51cRnJ6hxBlr/cKPckk+PKPUw/fXmvfKTcw7bwY3w9izgx5jZw==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.19.0.tgz", + "integrity": "sha512-mCFtBbFBJDCNCWUl5y6sZSCHXw1DEFEk3c/M3nRK2a4XUB8StGFtmcEMizdjKuBzB6e/smJAAWYug3VrdLMr1w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.18.2", + "@typescript-eslint/types": "8.19.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -1537,9 +1530,9 @@ } }, "node_modules/agentkeepalive": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", - "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", "license": "MIT", "optional": true, "dependencies": { @@ -1919,6 +1912,19 @@ "node": ">= 10" } }, + "node_modules/cacache/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", @@ -3238,21 +3244,21 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz", - "integrity": "sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", + "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", - "dunder-proto": "^1.0.0", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", + "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", - "math-intrinsics": "^1.0.0" + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -3261,6 +3267,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.0.tgz", + "integrity": "sha512-TtLgOcKaF1nMP2ijJnITkE4nRhbpshHhmzKiuhmSniiwWzovoqwqQ8rNuhf0mXJOqIY5iU+QkUe0CkJYrLsG9w==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-tsconfig": { "version": "4.8.1", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz", @@ -4014,19 +4033,6 @@ "node": ">=4" } }, - "node_modules/license-checker/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, "node_modules/license-checker/node_modules/nopt": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", @@ -4505,15 +4511,16 @@ } }, "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, "bin": { "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" } }, "node_modules/mkdirp-classic": { @@ -4584,6 +4591,26 @@ "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", "license": "MIT" }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-gyp": { "version": "8.4.1", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", @@ -6533,6 +6560,18 @@ "node": ">=8" } }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/teamcity-service-messages": { "version": "0.1.14", "resolved": "https://registry.npmjs.org/teamcity-service-messages/-/teamcity-service-messages-0.1.14.tgz", @@ -6786,15 +6825,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.18.2.tgz", - "integrity": "sha512-KuXezG6jHkvC3MvizeXgupZzaG5wjhU3yE8E7e6viOvAvD9xAWYp8/vy0WULTGe9DYDWcQu7aW03YIV3mSitrQ==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.19.0.tgz", + "integrity": "sha512-Ni8sUkVWYK4KAcTtPjQ/UTiRk6jcsuDhPpxULapUDi8A/l8TSBk+t1GtJA1RsCzIJg0q6+J7bf35AwQigENWRQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.18.2", - "@typescript-eslint/parser": "8.18.2", - "@typescript-eslint/utils": "8.18.2" + "@typescript-eslint/eslint-plugin": "8.19.0", + "@typescript-eslint/parser": "8.19.0", + "@typescript-eslint/utils": "8.19.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/package.json b/package.json index 29e307e..6b38fd6 100644 --- a/package.json +++ b/package.json @@ -6,18 +6,16 @@ "scripts": { "local-env-file": "bash ./src/misc/createEnvDev.sh", "start": "npm run local-env-file && tsx src/server.ts", - "start:build": "npx tsc && node dist/server.js", "dev": "npm run local-env-file && nodemon", "dev:trace": "npm run local-env-file && nodemon --trace-uncaught --trace-warnings", "dep": "bash ./src/misc/dependencyGraphs/createDependencyGraph.sh", "dep:remove": "bash ./src/misc/removeUnusedDeps.sh && npm run dep", "build": "npx tsc", "build:mini": "npx tsc && bash ./src/misc/minifyDist.sh --build-only", + "build:docker": "docker build . -t \"dockstatapi:local\" -f ./docker/Dockerfile-dev", "mini": "bash ./src/misc/minifyDist.sh", - "docker": "docker compose up -d", - "docker:full": "docker compose up -d && [ -z \"$TMUX\" ] && tmux new-session -d -s docker 'docker compose logs -f master' \\; split-window -v 'docker compose logs -f slave' \\; attach-session || echo 'Already inside a tmux session. Exiting.'; docker compose down", - "docker:build": "docker build . -t \"dockstatapi:local\" -f ./Dockerfile-dev && docker compose up -d", - "docker:build:full": "npm run docker:build && [ -z \"$TMUX\" ] && tmux new-session -d -s docker 'docker compose up -d && docker compose logs -f master' \\; split-window -v 'docker compose logs -f slave' \\; attach-session || echo 'Already inside a tmux session. Exiting.'; docker compose down", + "docker": "docker compose -f docker/docker-compose.yaml up -d && bash ./src/misc/.tmux.sh; docker compose -f docker/docker-compose.yaml down", + "docker:build": "npm run build:docker && npm run docker", "prettier": "npx prettier -c ./src/**/*.ts --parser typescript --write && npx prettier -c ./.github/workflows/*.yaml --parser yaml --write && npx prettier -c ./**/*.md --parser markdown --write && npx prettier -c ./**/*.json --parser json --write", "lint": "npx eslint", "lint:fix": "npx eslint --fix", diff --git a/src/config/hostsystem.ts b/src/config/hostsystem.ts index e9c04de..0af379f 100644 --- a/src/config/hostsystem.ts +++ b/src/config/hostsystem.ts @@ -1,4 +1,10 @@ -import { RUNNING_IN_DOCKER, VERSION } from "./variables"; +import { + RUNNING_IN_DOCKER, + VERSION, + HA_MASTER, + HA_UNSAFE, + TRUSTED_PROXYS, +} from "./variables"; import fs from "fs"; import logger from "../utils/logger"; import os from "os"; @@ -7,6 +13,8 @@ import { atomicWrite } from "../utils/atomicWrite"; const userConf = "./src/data/user.conf"; const inDocker: boolean = RUNNING_IN_DOCKER == "true"; const version: string = VERSION || "unknown"; +const masterNode: string = HA_MASTER === "true" ? "✓" : "✗"; +const unsafeSync: string = HA_UNSAFE === "true" ? "✓" : "✗"; function writeUserConf() { let previousConfig = null; @@ -54,9 +62,17 @@ function writeUserConf() { backendVersion: version, }; - logger.info( - `Starting at: ${startDetails.startedAt} - Version: ${startDetails.backendVersion} - Docker: ${installationDetails.inDocker} - Installed as: ${installationDetails.installedBy} - Platform: ${installationDetails.platform} - Arch: ${installationDetails.arch}`, - ); + logger.info("-----------------------------------------"); + logger.info(`Starting at : ${startDetails.startedAt}`); + logger.info(`Version : ${startDetails.backendVersion}`); + logger.info(`Docker : ${installationDetails.inDocker}`); + logger.info(`Running as : ${installationDetails.installedBy}`); + logger.info(`Platform : ${installationDetails.platform}`); + logger.info(`Arch : ${installationDetails.arch}`); + logger.info(`Master node : ${masterNode}`); + logger.info(`Unsafe sync : ${unsafeSync}`); + logger.info(`Proxies : ${TRUSTED_PROXYS}`); + logger.info("-----------------------------------------"); } export default writeUserConf; diff --git a/src/controllers/highAvailability.ts b/src/controllers/highAvailability.ts index 1b28b4f..3e61b16 100644 --- a/src/controllers/highAvailability.ts +++ b/src/controllers/highAvailability.ts @@ -172,7 +172,7 @@ async function synchronizeFilesWithNodes(): Promise { } const nodeUrl = - useUnsafeConnection == true + useUnsafeConnection === true ? `http://${node}/ha/sync` : `https://${node}/ha/sync`; @@ -259,7 +259,7 @@ async function ensureFileExists( const dirPath = path.dirname(filePath); await fs.promises.mkdir(dirPath, { recursive: true }); await fs.promises.writeFile(filePath, content, { flag: "w" }); - logger.info(`File created/updated: ${filePath}`); + logger.info(`File updated: ${filePath}`); } catch (error) { logger.error( `Error creating/updating file ${filePath}: ${(error as Error).message}`, diff --git a/src/misc/.tmux.sh b/src/misc/.tmux.sh new file mode 100644 index 0000000..a929a1a --- /dev/null +++ b/src/misc/.tmux.sh @@ -0,0 +1 @@ +[ -z "$TMUX" ] && tmux new-session -d -s docker 'docker compose -f docker/docker-compose.yaml logs -f master' \; rename-window 'master' \; new-window 'docker compose -f docker/docker-compose.yaml logs -f slave' \; rename-window 'slave' \; new-window 'docker compose -f docker/docker-compose.yaml logs -f test-socket-proxy' \; rename-window 'proxy' \; attach-session || echo 'Already inside a tmux session. Exiting.' diff --git a/src/server.ts b/src/server.ts index 6b68029..97e5337 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,5 +1,4 @@ import express from "express"; -import logger from "./utils/logger"; import initializeApp from "./init"; import { startMasterNode } from "./controllers/highAvailability"; import writeUserConf from "./config/hostsystem"; @@ -7,10 +6,6 @@ import writeUserConf from "./config/hostsystem"; const app = express(); const PORT: number = 9876; -logger.info("Server starting up..."); -logger.info(`Server is running on http://localhost:${PORT}`); -logger.info(`Swagger docs available at http://localhost:${PORT}/api-docs\n`); - writeUserConf(); initializeApp(app); From 5df68298e495951f4e5b2d5bc6242227ad26c8df Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 2 Jan 2025 22:01:10 +0100 Subject: [PATCH 100/324] Fix: Run uglifyjs only on .js files --- src/misc/minifyDist.sh | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/misc/minifyDist.sh b/src/misc/minifyDist.sh index 0c25617..8a85b16 100755 --- a/src/misc/minifyDist.sh +++ b/src/misc/minifyDist.sh @@ -3,28 +3,28 @@ dist="$(pwd)/dist" run_script() { - npx uglifyjs --no-annotations --in-situ "$1" > /dev/null - echo "✔️ Minified : $(basename "$1")" + npx uglifyjs --no-annotations --in-situ "$1" > /dev/null + echo "✔️ Minified : $(basename "$1")" } if [ -d "$dist" ]; then - echo "::: Dist directory exists." + echo "::: Dist directory exists." else - echo "::: Dist does not exist... Running npx tsc" - npx tsc + echo "::: Dist does not exist... Running npx tsc" + npx tsc fi max_jobs=$(nproc) job_count=0 -for file in $(find "$dist" -type f); do - run_script "$file" & - ((job_count++)) +for file in $(find "$dist" -type f -name "*.js"); do + run_script "$file" & + ((job_count++)) - if ((job_count >= max_jobs)); then - wait - job_count=0 - fi + if ((job_count >= max_jobs)); then + wait + job_count=0 + fi done wait From 7fa7eeb7bce75625585bbf81e7fd39f818b8d295 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 2 Jan 2025 22:19:23 +0100 Subject: [PATCH 101/324] Err: Workflow wont run --- .github/workflows/validation.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index 2226171..10adc68 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -168,7 +168,7 @@ jobs: contents: read runs-on: ubuntu-24.04 if: github.ref_name == 'dev' - needs: [validation, test-building, Anchore, CodeQL] + needs: [test-building] steps: - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -193,12 +193,12 @@ jobs: flavor: | latest=false - - name: Build and push + - name: Build and Push Docker Images uses: docker/build-push-action@v6 with: - platforms: linux/amd64,linux/arm64, context: . file: docker/Dockerfile-dev + platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.metadata.outputs.tags }} labels: ${{ steps.metadata.outputs.labels }} From 9175d9a99219abd241b40b35aecaf8560e87d031 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 3 Jan 2025 07:32:43 +0100 Subject: [PATCH 102/324] Fix: Update validation.yaml --- .github/workflows/validation.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index 10adc68..ab37d21 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -197,7 +197,7 @@ jobs: uses: docker/build-push-action@v6 with: context: . - file: docker/Dockerfile-dev + file: docker/Dockerfile-base platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.metadata.outputs.tags }} From f39f1bc50437fa10a64643b295118fab14058cc2 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 3 Jan 2025 07:48:56 +0100 Subject: [PATCH 103/324] Fix: Update validation.yaml --- .github/workflows/validation.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index ab37d21..a6e496d 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -170,6 +170,9 @@ jobs: if: github.ref_name == 'dev' needs: [test-building] steps: + - name: Checkout Repository + uses: actions/checkout@v3 + - name: Set up QEMU uses: docker/setup-qemu-action@v3 From 9bc2f651f54aa1b71b50ccb46ff1ff51863f8a9a Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 3 Jan 2025 08:11:13 +0100 Subject: [PATCH 104/324] Chore: Cleanup frontendConfiguration.json --- src/data/frontendConfiguration.json | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/src/data/frontendConfiguration.json b/src/data/frontendConfiguration.json index 884e0e2..fe51488 100644 --- a/src/data/frontendConfiguration.json +++ b/src/data/frontendConfiguration.json @@ -1,16 +1 @@ -[ - { - "name": "test", - "tags": [ - "123", - "123", - "321" - ], - "link": "https://google.com", - "icon": "custom/test.png" - }, - { - "name": "test2", - "pinned": true - } -] \ No newline at end of file +[] From bb8a627aedbdf684d286d0375c66e150e4c27638 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 3 Jan 2025 08:12:51 +0100 Subject: [PATCH 105/324] Fix: missing return in api.ts --- src/handlers/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/handlers/api.ts b/src/handlers/api.ts index 3752968..6f62c05 100644 --- a/src/handlers/api.ts +++ b/src/handlers/api.ts @@ -126,7 +126,7 @@ class ApiHandler { try { const rawData = fs.readFileSync(configPath); const data = JSON.parse(rawData.toString()); - ResponseHandler.rawData(data, "Fetched frontend configuration"); + return ResponseHandler.rawData(data, "Fetched frontend configuration"); } catch (error: unknown) { const errorMsg = error instanceof Error ? error.message : String(error); return ResponseHandler.critical(errorMsg); From 987b550f4c2b3c2961836476b2d5d974d76d1f8f Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 3 Jan 2025 09:15:15 +0100 Subject: [PATCH 106/324] Chore: Change to dev Dockerfile for workflow --- .github/workflows/validation.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index a6e496d..782a762 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -200,7 +200,7 @@ jobs: uses: docker/build-push-action@v6 with: context: . - file: docker/Dockerfile-base + file: docker/Dockerfile-dev platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.metadata.outputs.tags }} From 621d1426840fc525a0b3ed9df58a8a5a6092595b Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 3 Jan 2025 09:23:39 +0100 Subject: [PATCH 107/324] Feat: Added docker scout --- .github/workflows/validation.yaml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index 782a762..d15f1a4 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -159,6 +159,23 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max + - name: Analyze for critical and high CVEs + id: docker-scout-cves + if: ${{ github.event_name != 'pull_request_target' }} + uses: docker/scout-action@v1 + with: + command: cves + image: ${{ steps.meta.outputs.tags }} + sarif-file: sarif.output.json + summary: true + + - name: Upload SARIF result + id: upload-sarif + if: ${{ github.event_name != 'pull_request_target' }} + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: sarif.output.json + build-dev: name: "Dev-build" permissions: From d932c98ca15a17f665ef0637618fac110a6d0bca Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 3 Jan 2025 09:25:55 +0100 Subject: [PATCH 108/324] Feat: Update workflow logic --- .github/workflows/validation.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index d15f1a4..d683678 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -114,7 +114,7 @@ jobs: sarif_file: ./results.sarif test-building: - needs: [validation, Anchore, CodeQL] + needs: [validation] runs-on: ubuntu-24.04 name: "Test building" permissions: @@ -185,7 +185,7 @@ jobs: contents: read runs-on: ubuntu-24.04 if: github.ref_name == 'dev' - needs: [test-building] + needs: [test-building, Anchore, CodeQL] steps: - name: Checkout Repository uses: actions/checkout@v3 From c87ba873a7e121643694e2d693e90d3a8987daac Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 3 Jan 2025 09:31:23 +0100 Subject: [PATCH 109/324] Fix: Permissions --- .github/workflows/validation.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index d683678..6016534 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -121,7 +121,7 @@ jobs: security-events: write packages: read actions: read - contents: read + contents: write steps: - name: Checkout repository uses: actions/checkout@v4 From 9cb349059663cd4a210cbe13d1adadfa236ea656 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 3 Jan 2025 09:36:34 +0100 Subject: [PATCH 110/324] Fix: Workflow adjustment => drop docker scout :/ --- .github/workflows/validation.yaml | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index 6016534..7414ee5 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -159,23 +159,6 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max - - name: Analyze for critical and high CVEs - id: docker-scout-cves - if: ${{ github.event_name != 'pull_request_target' }} - uses: docker/scout-action@v1 - with: - command: cves - image: ${{ steps.meta.outputs.tags }} - sarif-file: sarif.output.json - summary: true - - - name: Upload SARIF result - id: upload-sarif - if: ${{ github.event_name != 'pull_request_target' }} - uses: github/codeql-action/upload-sarif@v2 - with: - sarif_file: sarif.output.json - build-dev: name: "Dev-build" permissions: From 7b09298399a903590210acb1b1abb4b5168b6ae2 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 3 Jan 2025 09:47:03 +0100 Subject: [PATCH 111/324] Fix: Remove unnecessary matrix --- .github/workflows/validation.yaml | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index 7414ee5..7392c83 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -49,13 +49,6 @@ jobs: actions: read contents: read - strategy: - fail-fast: false - matrix: - include: - - language: javascript-typescript - build-mode: none - steps: - name: Checkout repository uses: actions/checkout@v4 @@ -64,20 +57,9 @@ jobs: uses: github/codeql-action/init@v3 with: languages: javascript-typescript - build-mode: ${{ matrix.build-mode }} + build-mode: none queries: security-extended - - name: Check build mode - if: matrix.build-mode == 'manual' - shell: bash - run: | - echo 'If you are using a "manual" build mode for one or more of the' \ - 'languages you are analyzing, replace this with the commands to build' \ - 'your code, for example:' - echo ' make bootstrap' - echo ' make release' - exit 1 - - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 with: From 204d4bf037bcd8225c8cbf22117d8faf59bdfcc0 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 3 Jan 2025 14:27:59 +0100 Subject: [PATCH 112/324] Chore: Pre release workflow --- .github/workflows/validation.yaml | 55 ++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index 7392c83..1763ee9 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -1,6 +1,10 @@ name: "Run all tests" -on: [push] +on: + push: + release: + types: + - published jobs: validation: @@ -189,3 +193,52 @@ jobs: labels: ${{ steps.metadata.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max + + build-pre-release: + name: "Pre-Release-build" + permissions: + security-events: read + packages: write + actions: read + contents: read + runs-on: ubuntu-24.04 + if: "!github.event.release.prerelease" + needs: [test-building, Anchore, CodeQL] + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Github Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ github.token }} + + - name: Generate Docker tags + uses: docker/metadata-action@v5 + id: metadata + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=raw,enable=true,priority=200,prefix=,suffix=,value=pre + flavor: | + latest=false + + - name: Build and Push Docker Images + uses: docker/build-push-action@v6 + with: + context: . + file: docker/Dockerfile-dev + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.metadata.outputs.tags }} + labels: ${{ steps.metadata.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max From b27369fdc2697b04ff9b847d1da957e08f3e825a Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 3 Jan 2025 14:34:57 +0100 Subject: [PATCH 113/324] Fix: removed negation from if in workflow --- .github/workflows/validation.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index 1763ee9..7040e94 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -202,7 +202,7 @@ jobs: actions: read contents: read runs-on: ubuntu-24.04 - if: "!github.event.release.prerelease" + if: "github.event.release.prerelease" needs: [test-building, Anchore, CodeQL] steps: - name: Checkout Repository From fe37f2daa0ef3068348788af4aa874824399d693 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sun, 5 Jan 2025 14:16:25 +0100 Subject: [PATCH 114/324] Feat: Added new image => See next commit --- .github/DockStat-dark.png | Bin 0 -> 82847 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .github/DockStat-dark.png diff --git a/.github/DockStat-dark.png b/.github/DockStat-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..00ac779a6dcb303ce5c690aa079dedc175e5c8f3 GIT binary patch literal 82847 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

TLgl9u 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 AX#fBK literal 0 HcmV?d00001 From a82d021d68ee30fe949dc35e894f20932223fe8e Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 29 Jan 2025 20:19:01 +0100 Subject: [PATCH 115/324] Fix: Add checkout --- .github/workflows/build-image.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build-image.yaml b/.github/workflows/build-image.yaml index 41f18cb..5e0db4c 100644 --- a/.github/workflows/build-image.yaml +++ b/.github/workflows/build-image.yaml @@ -12,6 +12,9 @@ jobs: build-release: runs-on: ubuntu-24.04 steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Set up QEMU uses: docker/setup-qemu-action@v3 From 0f3ed69ac6d319fdbcbcb3d841cf4b637dcbc57b Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sun, 9 Feb 2025 14:37:41 +0100 Subject: [PATCH 116/324] Feat: Add WebSockets, graph generation service and HA (#31) * Chore: new testing workflows (dropping playwright) * Chore: updating github workflow * Fix: Fixing some minor things * Chore: Updated to ES2020 syntax and AMD module * Feat: startServer function to start the server with a different port * Fix: Adjusting testing files based on workflow restrictions * Fix: Adjusting testing files based on workflow restrictions * Chore: Updating swagger (wrong branch bruh) * Docs: Update swagger documentation (#26) * Chore: Updated swagger * Fix: Typo * Fix: Fixing dockerfiles for prod/dev environment * Feat: Add `/graph` and `/graph/image` endpoints (#27) * Feat: Server side HTML generation => Client side rendering * Fix: This _might_ fix the workflow * Fix: Remove unused function * Fix: Please make it stop * Fix: Setting up python before hand * Fix: Remove unused dep * Fix: Using node20 instead of latest * Fix: Works on my end... * Feat: Master Nodes * Feat: Icon for master node (needs testing) * Fix: Adjusting function (needs testing) * Fix: Adjusting function (needs testing) * Fix: Removed some graph rendering features (will be back but better) * Feat: render html file as png using puppeteer ToFix: svgs dont render * Fix: Hell yeah we got image creation! * Fix: Adjusted routes since they need an absolute path * Fix: Remove unused dependencies * Fix: Exclude CWE-200 from CodeQl * Feat: Respomse examples in swagger Fix: Fixing some catch blocks * Fix: Adjusting catches * Fix: Adjusted catch to typing * Feat: Stack creation * Feat: Stack creation + starting and stopping * Fix: Project root instead of path Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Fix: Logging adustment * Fix: Allow undescores and dashes in stack name Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Fix: Propagate error * Fix: Inline variable that is immediately returned Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Fix: move some things around * Fix: Minor adjustments * Feat: Get a stack's docker-compose * Feat: automatic Stack environmental file management * Fix: sample-varaible.json adjustment * Fix: Potential fix for code scanning alert no. 102: Log injection Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Fix: fix for code scanning alert no. 94: Uncontrolled data used in path expression Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Fix: fix for code scanning alert no. 92: Uncontrolled data used in path expression Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * FiX: fix for code scanning alert no. 106: Log injection Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Fix: Logger vulnerability and CI graph generation * Feat: change logger verbosity and spelling fix * Feat: ToDo comments to GH issue * Fix: Add checkout * Fix: May fix the ToDo workflow * Fix: Remove todo * Fix: Re-Add commit * Fix: Remove TODO * Fix: Re-add TODO * Fix: Where tf did my package lock go :sob: * CI/CD: Remove ToDo * CI/CD: Add ToDo * CI/CD: Fix command * CI/CD: Add checkout * Fix: CPU value was a percentage the whole time? * Feat: Websocket endpoints for logs and container metrics * Fix: Make linter happy * Fix: Fix import * Fix: Fix tsc build * Jest: Fix tests * Jest: Fix Tests * Fix: Typo in src/config/swagger.yaml Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Fix: Typo in src/config/swagger.yaml Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Fix: Tyypo in src/config/swagger.yaml Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * (code-quality): Inline variable that is immediately returned Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * (code-quality): Prefer object destructuring when accessing and using properties. Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * (code-quality): Prefer object destructuring when accessing and using properties. Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * (code-quality): Prefer object destructuring when accessing and using properties. Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Fix: Update extractHostData.ts * Update TODO.md --------- Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: ItsNik --- .dockerignore | 7 +- .github/workflows/build-image.yaml | 24 +- .github/workflows/validation.yaml | 67 +- .gitignore | 3 + CREDITS.md | 106 +- README.md | 1 + TODO.md | 5 +- __tests__/auth.spec.ts | 38 + __tests__/config.spec.ts | 49 + __tests__/database.spec.ts | 35 + __tests__/frontend.spec.ts | 125 + __tests__/getters.spec.ts | 99 + __tests__/util/previousResponse.ts | 23 + docker/Dockerfile-base | 19 +- docker/Dockerfile-dev | 19 +- docker/docker-compose.dev.yaml | 40 + docker/docker-compose.yaml | 1 + environment.d.ts | 38 +- eslint.config.mjs | 2 +- nodemon.json | 3 +- package-lock.json | 9462 ++++++++++++++--- package.json | 74 +- playwright.config.ts | 37 - src/config/db.ts | 2 +- src/config/hostsystem.ts | 21 +- src/config/initFiles.ts | 1 + src/config/stacks.ts | 260 + src/config/swagger.yaml | 2095 ++++ src/config/swaggerConfig.ts | 59 +- src/config/swaggerTheme.ts | 6 + src/config/variables.ts | 4 +- src/controllers/containerController.ts | 2 +- src/controllers/fetchData.ts | 7 +- src/controllers/frontendConfiguration.ts | 46 +- src/controllers/highAvailability.ts | 38 +- src/controllers/proxy.ts | 4 +- src/controllers/scheduler.ts | 8 +- src/data/frontendConfiguration.json | 2 +- src/handlers/api.ts | 8 +- src/handlers/conf.ts | 9 +- src/handlers/data.ts | 30 + src/handlers/graph.ts | 257 + src/handlers/notification.ts | 5 +- src/handlers/response.ts | 2 +- src/handlers/stack.ts | 162 + src/init.ts | 36 +- src/middleware/authMiddleware.ts | 2 +- src/misc/createEnvDev.sh | 14 +- src/misc/createEnvFile.sh | 14 +- .../dependencyGraphs/createDependencyGraph.sh | 2 +- src/misc/dependencyGraphs/mermaid-all.txt | 166 +- src/misc/dependencyGraphs/mermaid-graph.txt | 15 + src/misc/entrypoint.sh | 8 +- src/misc/minifyDist.sh | 2 +- src/routes/auth/routes.ts | 42 - src/routes/data/routes.ts | 134 - src/routes/frontendController/routes.ts | 441 - src/routes/getter/routes.ts | 273 - src/routes/graphs/routes.ts | 31 + src/routes/highavailability/routes.ts | 30 - src/routes/notifications/routes.ts | 109 - src/routes/setter/routes.ts | 73 +- src/routes/stack/routes.ts | 35 + src/sample-variable.json | 6 +- src/server.ts | 18 +- src/typings/dockerCompose.ts | 92 + src/typings/dockerStackEnv.ts | 10 + src/typings/ha.ts | 2 +- src/typings/stackConfig.ts | 5 + src/utils/assets/api-icon.svg | 1 + src/utils/assets/container-icon.svg | 1 + src/utils/assets/server-icon.svg | 1 + src/utils/atomicWrite.ts | 2 +- src/utils/connectionChecker.ts | 4 +- src/utils/containerService.ts | 223 +- src/utils/dockerClient.ts | 14 +- src/utils/extractHostData.ts | 73 +- src/utils/logger.ts | 10 +- src/utils/notifications/_template.ts | 9 +- src/utils/notifications/email.ts | 3 +- src/utils/startServer.ts | 18 + src/utils/swaggerDocs.ts | 9 +- src/utils/webSocket.ts | 113 + tests/main.spec.ts | 131 - tsconfig.json | 2 +- 85 files changed, 12086 insertions(+), 3393 deletions(-) create mode 100644 __tests__/auth.spec.ts create mode 100644 __tests__/config.spec.ts create mode 100644 __tests__/database.spec.ts create mode 100644 __tests__/frontend.spec.ts create mode 100644 __tests__/getters.spec.ts create mode 100644 __tests__/util/previousResponse.ts create mode 100644 docker/docker-compose.dev.yaml delete mode 100644 playwright.config.ts create mode 100644 src/config/stacks.ts create mode 100644 src/config/swagger.yaml create mode 100644 src/config/swaggerTheme.ts create mode 100644 src/handlers/graph.ts create mode 100644 src/handlers/stack.ts create mode 100644 src/misc/dependencyGraphs/mermaid-graph.txt create mode 100644 src/routes/graphs/routes.ts create mode 100644 src/routes/stack/routes.ts create mode 100644 src/typings/dockerCompose.ts create mode 100644 src/typings/dockerStackEnv.ts create mode 100644 src/typings/stackConfig.ts create mode 100644 src/utils/assets/api-icon.svg create mode 100644 src/utils/assets/container-icon.svg create mode 100644 src/utils/assets/server-icon.svg create mode 100644 src/utils/startServer.ts create mode 100644 src/utils/webSocket.ts delete mode 100644 tests/main.spec.ts diff --git a/.dockerignore b/.dockerignore index 2d99309..6381947 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,11 +1,12 @@ # custom paths: src/data/* -*.md -*.txt -docker +.tmp +docker/master +docker/slave .test* # Created by https://www.toptal.com/developers/gitignore/api/node ### Node ### +*-audit.json # Logs logs *.log diff --git a/.github/workflows/build-image.yaml b/.github/workflows/build-image.yaml index 5e0db4c..bbb4875 100644 --- a/.github/workflows/build-image.yaml +++ b/.github/workflows/build-image.yaml @@ -1,4 +1,4 @@ -name: "Build dockstatapi:latest" +name: "Build and Push Docker Image" on: release: @@ -21,22 +21,34 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Login to Github Container Registry + - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ github.token }} - - name: Generate Docker tags + - name: Extract version and create tag + id: get-tag + run: | + # Remove 'v' prefix from release tag if present + VERSION="${GITHUB_REF#refs/tags/v}" + # Check if pre-release and append '-pre' + if ${{ github.event.release.prerelease }}; then + TAG="$VERSION-pre" + else + TAG="$VERSION" + fi + echo "tag=$TAG" >> $GITHUB_OUTPUT + + - name: Generate Docker metadata uses: docker/metadata-action@v5 id: metadata with: images: ghcr.io/${{ github.repository }} tags: | - type=sha,format=long,prefix= - flavor: | - latest=true + type=raw,value=${{ steps.get-tag.outputs.tag }} + type=raw,value=latest,enable=${{ !github.event.release.prerelease }} - name: Build and push uses: docker/build-push-action@v6 diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index 7040e94..7e2b685 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -26,7 +26,7 @@ jobs: cache: npm - name: Install dependencies - run: npm ci --ignore-scripts + run: npm ci - name: Create varaibles.json run: npm run local-env-file @@ -43,8 +43,43 @@ jobs: - name: Audit packages run: npm audit --audit-level=high - CodeQL: + - name: Jests + run: npm run test:silent + + ToDo: needs: validation + runs-on: ubuntu-20.04 + name: "ToDo comment to issue" + permissions: + contents: write + issues: write + pull-requests: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: "TODO to Issue" + uses: "alstr/todo-to-issue-action@v5" + with: + INSERT_ISSUE_URLS: "true" + + - name: Set Git user + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + - name: Commit and Push Changes + run: | + git add -A + if [[ `git status --porcelain` ]]; then + git commit -m "Automatically added GitHub issue links to TODOs" + git push + else + echo "No changes to commit" + fi + + CodeQL: + needs: [ToDo] runs-on: ubuntu-24.04 name: "Analyze TypeScript" permissions: @@ -57,12 +92,21 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Setup python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: javascript-typescript build-mode: none queries: security-extended + config: | + query-filter: + - exclude: + tags: /cwe-200/ - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 @@ -70,7 +114,7 @@ jobs: category: "/language:javascript-typescript" Anchore: - needs: validation + needs: [ToDo] runs-on: ubuntu-24.04 name: "Anchore" permissions: @@ -82,6 +126,11 @@ jobs: - name: Set up Grype installation path run: echo "$HOME/bin" >> $GITHUB_PATH + - name: Setup python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + - name: Download Grype run: | curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b $HOME/bin @@ -100,7 +149,7 @@ jobs: sarif_file: ./results.sarif test-building: - needs: [validation] + needs: [ToDo] runs-on: ubuntu-24.04 name: "Test building" permissions: @@ -112,6 +161,11 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Setup python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -159,6 +213,11 @@ jobs: - name: Checkout Repository uses: actions/checkout@v3 + - name: Setup python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + - name: Set up QEMU uses: docker/setup-qemu-action@v3 diff --git a/.gitignore b/.gitignore index dc93b88..9e264ac 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,12 @@ # custom paths: src/data/* +src/data/frontendConfiguration.json +.tmp docker/master docker/slave .test* +stacks # Created by https://www.toptal.com/developers/gitignore/api/node ### Node ### *-audit.json diff --git a/CREDITS.md b/CREDITS.md index 050b430..50b66ab 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -8,35 +8,81 @@ This file shows all npm packages used in DockStatAPI (also Dev packages) | ----------------- | -------------------------------------------- | -------------------- | | spdx-ranges@2.1.1 | https://github.com/kemitchell/spdx-ranges.js | The Linux Foundation | +### License: Apache 2.0 + +| Name | Repository | Publisher | +| ---------------------- | ------------------------------------------ | --------- | +| qrcode-terminal@0.12.0 | https://github.com/gtanner/qrcode-terminal | N/A | + ### License: Apache-2.0 -| Name | Repository | Publisher | -| ------------------------------------ | ------------------------------------------------------------- | --------------------- | -| @balena/dockerignore@1.0.2 | https://github.com/balena-io-modules/dockerignore | N/A | -| @eslint/config-array@0.19.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/core@0.9.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/object-schema@2.1.5 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/plugin-kit@0.2.4 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @humanfs/core@0.19.1 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | -| @humanfs/node@0.16.6 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | -| @humanwhocodes/module-importer@1.0.1 | https://github.com/humanwhocodes/module-importer | Nicholas C. Zaks | -| @humanwhocodes/retry@0.3.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | -| @humanwhocodes/retry@0.4.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | -| @playwright/test@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | -| @scarf/scarf@1.4.0 | https://github.com/scarf-sh/scarf-js | Scarf Systems | -| detect-libc@2.0.3 | https://github.com/lovell/detect-libc | Lovell Fuller | -| docker-modem@5.0.3 | https://github.com/apocas/docker-modem | Pedro Dias | -| dockerode@4.0.2 | https://github.com/apocas/dockerode | Pedro Dias | -| doctrine@3.0.0 | https://github.com/eslint/doctrine | N/A | -| eslint-visitor-keys@3.4.3 | https://github.com/eslint/eslint-visitor-keys | Toru Nagashima | -| eslint-visitor-keys@4.2.0 | https://github.com/eslint/js | Toru Nagashima | -| playwright-core@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | -| playwright@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | -| spdx-correct@3.2.0 | https://github.com/jslicense/spdx-correct.js | N/A | -| swagger-ui-dist@5.18.2 | https://github.com/swagger-api/swagger-ui | N/A | -| tunnel-agent@0.6.0 | https://github.com/mikeal/tunnel-agent | Mikeal Rogers | -| typescript@5.7.2 | https://github.com/microsoft/TypeScript | Microsoft Corp. | -| validate-npm-package-license@3.0.4 | https://github.com/kemitchell/validate-npm-package-license.js | Kyle E. Mitchell | +| Name | Repository | Publisher | +| ------------------------------------ | ------------------------------------------------------------------------ | -------------------- | +| @ampproject/remapping@2.3.0 | https://github.com/ampproject/remapping | Justin Ridgewell | +| @balena/dockerignore@1.0.2 | https://github.com/balena-io-modules/dockerignore | N/A | +| @eslint/config-array@0.19.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/core@0.9.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/object-schema@2.1.5 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/plugin-kit@0.2.4 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @humanfs/core@0.19.1 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | +| @humanfs/node@0.16.6 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | +| @humanwhocodes/module-importer@1.0.1 | https://github.com/humanwhocodes/module-importer | Nicholas C. Zaks | +| @humanwhocodes/retry@0.3.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | +| @humanwhocodes/retry@0.4.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | +| @puppeteer/browsers@2.7.0 | https://github.com/puppeteer/puppeteer/tree/main/packages/browsers | The Chromium Authors | +| @scarf/scarf@1.4.0 | https://github.com/scarf-sh/scarf-js | Scarf Systems | +| @sigstore/bundle@3.0.0 | https://github.com/sigstore/sigstore-js | bdehamer@github.com | +| @sigstore/core@2.0.0 | https://github.com/sigstore/sigstore-js | bdehamer@github.com | +| @sigstore/protobuf-specs@0.3.2 | https://github.com/sigstore/protobuf-specs | bdehamer@github.com | +| @sigstore/sign@3.0.0 | https://github.com/sigstore/sigstore-js | bdehamer@github.com | +| @sigstore/tuf@3.0.0 | https://github.com/sigstore/sigstore-js | bdehamer@github.com | +| @sigstore/verify@2.0.0 | https://github.com/sigstore/sigstore-js | bdehamer@github.com | +| b4a@1.6.7 | https://github.com/holepunchto/b4a | Holepunch | +| bare-events@2.5.4 | https://github.com/holepunchto/bare-events | Holepunch | +| bare-fs@2.3.5 | https://github.com/holepunchto/bare-fs | Holepunch | +| bare-os@2.4.4 | https://github.com/holepunchto/bare-os | Holepunch | +| bare-path@2.1.3 | https://github.com/holepunchto/bare-path | Holepunch | +| bare-stream@2.6.1 | https://github.com/holepunchto/bare-stream | Holepunch | +| bser@2.1.1 | https://github.com/facebook/watchman | Wez Furlong | +| chromium-bidi@0.11.0 | https://github.com/GoogleChromeLabs/chromium-bidi | The Chromium Authors | +| chromium-bidi@0.12.0 | https://github.com/GoogleChromeLabs/chromium-bidi | The Chromium Authors | +| detect-libc@2.0.3 | https://github.com/lovell/detect-libc | Lovell Fuller | +| docker-modem@5.0.3 | https://github.com/apocas/docker-modem | Pedro Dias | +| dockerode@4.0.2 | https://github.com/apocas/dockerode | Pedro Dias | +| ejs@3.1.10 | https://github.com/mde/ejs | Matthew Eernisse | +| eslint-visitor-keys@3.4.3 | https://github.com/eslint/eslint-visitor-keys | Toru Nagashima | +| eslint-visitor-keys@4.2.0 | https://github.com/eslint/js | Toru Nagashima | +| exponential-backoff@3.1.1 | https://github.com/coveo/exponential-backoff | Sami Sayegh | +| fb-watchman@2.0.2 | https://github.com/facebook/watchman | Wez Furlong | +| filelist@1.0.4 | https://github.com/mde/filelist | Matthew Eernisse | +| human-signals@2.1.0 | https://github.com/ehmicky/human-signals | ehmicky | +| jake@10.9.2 | https://github.com/jakejs/jake | Matthew Eernisse | +| puppeteer-core@24.0.0 | https://github.com/puppeteer/puppeteer/tree/main/packages/puppeteer-core | The Chromium Authors | +| puppeteer@24.0.0 | https://github.com/puppeteer/puppeteer/tree/main/packages/puppeteer | The Chromium Authors | +| sigstore@3.0.0 | https://github.com/sigstore/sigstore-js | bdehamer@github.com | +| spdx-correct@3.2.0 | https://github.com/jslicense/spdx-correct.js | N/A | +| swagger-ui-dist@5.18.2 | https://github.com/swagger-api/swagger-ui | N/A | +| text-decoder@1.2.3 | https://github.com/holepunchto/text-decoder | Holepunch | +| tunnel-agent@0.6.0 | https://github.com/mikeal/tunnel-agent | Mikeal Rogers | +| typescript@5.7.2 | https://github.com/microsoft/TypeScript | Microsoft Corp. | +| validate-npm-package-license@3.0.4 | https://github.com/kemitchell/validate-npm-package-license.js | Kyle E. Mitchell | +| walker@1.0.8 | https://github.com/daaku/nodejs-walker | Naitik Shah | + +### License: Artistic-2.0 + +| Name | Repository | Publisher | +| ---------- | -------------------------- | ----------- | +| npm@11.0.0 | https://github.com/npm/cli | GitHub Inc. | + +### License: BlueOak-1.0.0 + +| Name | Repository | Publisher | +| ---------------------------- | ------------------------------------------------ | ------------------ | +| chownr@3.0.0 | https://github.com/isaacs/chownr | Isaac Z. Schlueter | +| jackspeak@3.4.3 | https://github.com/isaacs/jackspeak | Isaac Z. Schlueter | +| package-json-from-dist@1.0.1 | https://github.com/isaacs/package-json-from-dist | Isaac Z. Schlueter | +| path-scurry@1.11.1 | https://github.com/isaacs/path-scurry | Isaac Z. Schlueter | +| yallist@5.0.0 | https://github.com/isaacs/yallist | Isaac Z. Schlueter | ### License: CC-BY-3.0 @@ -44,6 +90,12 @@ This file shows all npm packages used in DockStatAPI (also Dev packages) | --------------------- | -------------------------------------------------- | -------------------- | | spdx-exceptions@2.5.0 | https://github.com/kemitchell/spdx-exceptions.json | The Linux Foundation | +### License: CC-BY-4.0 + +| Name | Repository | Publisher | +| ------------------------- | -------------------------------------------- | ---------- | +| caniuse-lite@1.0.30001690 | https://github.com/browserslist/caniuse-lite | Ben Briggs | + ### License: Python-2.0 | Name | Repository | Publisher | diff --git a/README.md b/README.md index 8fc800a..2577866 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ With this new release a couple of extra features (compared to v1) are going to b - Advanced security through middlewares: rate-limiting and authentication - Multi Arch Docker builds through docker buildx - High Availability using single master and unlimited worker nodes! +- Dynamically created Graphs # 🔗 DockStatAPI v2 Documentation diff --git a/TODO.md b/TODO.md index 7ac3d43..44a128d 100644 --- a/TODO.md +++ b/TODO.md @@ -7,11 +7,12 @@ - [x] Structure code differently - [x] Write new README and make the docs better - [x] Update more files to correct TS syntax => remove "any" -- [ ] Websockets +- [X] Websockets - [x] Better /api/status endpoint with connection status of each host - [x] Update notification service - [x] Adjust process.env variables since they don't really work as expected (See [commit](https://github.com/Its4Nik/dockstatapi/pull/21/commits/a03b58c7a17e269f46216df5492e18d008774961)) - [ ] Better project structure - [x] Update logging => Better errors - [x] Update json responses -- [ ] Swagger update +- [X] Swagger update +- [ ] Edge case testing diff --git a/__tests__/auth.spec.ts b/__tests__/auth.spec.ts new file mode 100644 index 0000000..bcf0eb2 --- /dev/null +++ b/__tests__/auth.spec.ts @@ -0,0 +1,38 @@ +export const testPass = "123456789"; +import { Server } from 'http'; +import supertest from "supertest"; +import { startServer } from "../src/utils/startServer"; +import app from "../src/server"; + +const port = 13001; +const server = new Server(app); + +startServer(app, server, port); + +const request = supertest(`http://localhost:${port}`); + +describe("Authentication", () => { + it("Enable Authentication", async () => { + const res = await request.post(`/auth/enable?password=${testPass}`); + expect(res.status).toEqual(200); + expect(res.type).toEqual(expect.stringContaining("json")); + expect(res.body).toHaveProperty( + "message", + "Authentication enabled successfully", + ); + }); + + it("Test no password", async () => { + const res = await request.get("/api/status"); + expect(res.status).toEqual(403); + expect(res.type).toEqual(expect.stringContaining("json")); + }); + + it("Disable authentication", async () => { + const res = await request + .post(`/auth/disable?password=${testPass}`) + .set("x-password", testPass); + expect(res.status).toEqual(200); + expect(res.type).toEqual(expect.stringContaining("json")); + }); +}); \ No newline at end of file diff --git a/__tests__/config.spec.ts b/__tests__/config.spec.ts new file mode 100644 index 0000000..d635600 --- /dev/null +++ b/__tests__/config.spec.ts @@ -0,0 +1,49 @@ +import supertest from "supertest"; +import { startServer } from "../src/utils/startServer"; +import app from "../src/server"; +import { Server } from 'http'; + +const port = 13002; +const server = new Server(app); + +startServer(app, server, port); + +const request = supertest(`http://localhost:${port}`); + +const mockServerName: string = "mockstatapi"; +const mockServerIP: string = "127.0.0.1"; +const mockServerPort: number = 2375; + +describe("Config endpoints", () => { + it("Add an host", async () => { + let res = await request.put( + `/conf/addHost?name=${mockServerName}&url=${mockServerIP}&port=${mockServerPort}`, + ); + expect(res.status).toEqual(200); + + res = await request.get("/api/hosts"); + expect(res.status).toEqual(200); + expect(res.body).toContain("mockstatapi"); + }); + + it("Adjust scheduler", async () => { + let res = await request.put("/conf/scheduler?interval=10m"); + expect(res.status).toEqual(200); + + res = await request.get("/api/current-schedule"); + expect(res.status).toEqual(200); + + // Reset to standart 5m + res = await request.put("/conf/scheduler?interval=5m"); + expect(res.status).toEqual(200); + }); + + it("Remove Host from config", async () => { + let res = await request.delete(`/conf/removeHost?hostName=mockstatapi`); + expect(res.status).toEqual(200); + + res = await request.get("/api/hosts"); + expect(res.status).toEqual(200); + expect(res.body).not.toHaveProperty("mockstatapi"); + }); +}); diff --git a/__tests__/database.spec.ts b/__tests__/database.spec.ts new file mode 100644 index 0000000..c0c46c1 --- /dev/null +++ b/__tests__/database.spec.ts @@ -0,0 +1,35 @@ +import supertest from "supertest"; +import { startServer } from "../src/utils/startServer"; +import app from "../src/server"; +import { Server } from 'http'; + +const port = 13003; +const server = new Server(app); + +startServer(app, server, port); + +const request = supertest(`http://localhost:${port}`); + +describe("Database", () => { + it("Get latest database entry", async () => { + const res = await request.get("/data/latest"); + expect(res.status).toEqual(200); + }); + + it("Get all database entries", async () => { + const res = await request.get("/data/all"); + expect(res.status).toEqual(200); + }); + + it("Clear database", async () => { + let res = await request.delete("/data/clear"); + expect(res.status).toEqual(200); + + res = await request.get("/data/latest"); + expect(res.status).toEqual(404); + expect(res.body).toHaveProperty( + "message", + "No data available for /data/latest", + ); + }); +}); diff --git a/__tests__/frontend.spec.ts b/__tests__/frontend.spec.ts new file mode 100644 index 0000000..753b98d --- /dev/null +++ b/__tests__/frontend.spec.ts @@ -0,0 +1,125 @@ +import supertest from "supertest"; +import { startServer } from "../src/utils/startServer"; +import app from "../src/server"; +import { Server } from 'http'; + +const port = 13004; +const server = new Server(app); + +startServer(app, server, port); + +const request = supertest(`http://localhost:${port}`); + +const sec: number = 1000; + +const mockContainer: string = "dockstatapi"; +const mockLink: string = "https://github.com/its4nik/dockstatapi"; +const mockIcon: string = "dockstatapi.png"; +const mockTag1: string = "backend"; +const mockTag2: string = "local"; + +const verifiedResponse = [ + { + name: "dockstatapi", + tags: ["backend", "local"], + pinned: true, + link: "https://github.com/its4nik/dockstatapi", + icon: "dockstatapi.png", + hidden: true, + }, +]; + + + +describe("Test frontend specific configurations", () => { + it( + "Setup the configuration file", + async () => { + // Hide container + let res = await request.delete(`/frontend/hide/${mockContainer}`); + + expect(res.status).toEqual(200); + + // Add Tag(s) + res = await request.post(`/frontend/tag/${mockContainer}/${mockTag1}`); + + expect(res.status).toEqual(200); + res = await request.post(`/frontend/tag/${mockContainer}/${mockTag2}`); + + expect(res.status).toEqual(200); + + // Pin container + res = await request.post(`/frontend/pin/${mockContainer}`); + + expect(res.status).toEqual(200); + + // Add link + res = await request.post( + `/frontend/add-link/${mockContainer}/${encodeURIComponent(mockLink)}`, + ); + + expect(res.status).toEqual(200); + + // Add icon + res = await request.post( + `/frontend/add-icon/${mockContainer}/${mockIcon}/false`, + ); + + expect(res.status).toEqual(200); + }, + 60 * sec, + ); + + it("Verify the configuration", async () => { + const res = await request.get("/api/frontend-config"); + + expect(res.status).toEqual(200); + expect(res.body).toEqual(verifiedResponse); + }); + + it( + "Reset configuration", + async () => { + // Show container + let res = await request.post(`/frontend/show/${mockContainer}`); + + expect(res.status).toEqual(200); + + // Remove tag(s) + res = await request.delete( + `/frontend/remove-tag/${mockContainer}/${mockTag1}`, + ); + + expect(res.status).toEqual(200); + + res = await request.delete( + `/frontend/remove-tag/${mockContainer}/${mockTag2}`, + ); + + expect(res.status).toEqual(200); + + // Unpin + res = await request.delete(`/frontend/unpin/${mockContainer}`); + + expect(res.status).toEqual(200); + + // Remove link + res = await request.delete(`/frontend/remove-link/${mockContainer}`); + + expect(res.status).toEqual(200); + + // Remove icon + res = await request.delete(`/frontend/remove-icon/${mockContainer}`); + + expect(res.status).toEqual(200); + }, + 60 * sec, + ); + + it("Verify the reset configuration", async () => { + const res = await request.get("/api/frontend-config"); + + expect(res.status).toEqual(200); + expect(res.body).toEqual([]); + }); +}); diff --git a/__tests__/getters.spec.ts b/__tests__/getters.spec.ts new file mode 100644 index 0000000..3ba5950 --- /dev/null +++ b/__tests__/getters.spec.ts @@ -0,0 +1,99 @@ +import { createPreviousResponse } from "./util/previousResponse"; +import supertest from "supertest"; +import { startServer } from "../src/utils/startServer"; +import app from "../src/server"; +import { Server } from 'http'; + +const port = 13005; +const server = new Server(app); + +startServer(app, server, port); + +const request = supertest(`http://localhost:${port}`); +const PreviousResponse = createPreviousResponse(); + +describe("Get endpoints", () => { + it("GET /api/hosts", async () => { + const res = await request.get("/api/hosts"); + expect(res.status).toEqual(200); + expect(res.type).toEqual(expect.stringContaining("json")); + + const hosts: string[] = res.body; + + if (hosts.length >= 1) { + expect(Array.isArray(hosts)).toBe(true); + expect(hosts.length).toBeGreaterThan(0); + expect(typeof hosts[0]).toBe("string"); + PreviousResponse.set(hosts[0]); + } + }); + + it("GET /api/host/:host/stats", async () => { + const host = PreviousResponse.get(); + + if (!host) { + console.log("No hosts found, skipping /api/host/:host/stats test"); + return; + } + + const res = await request.get(`/api/host/${host}/stats`); + + expect(res.status).toEqual(200); + expect(res.type).toEqual(expect.stringContaining("json")); + }); + + it("GET /api/system", async () => { + const res = await request.get("/api/system"); + expect(res.status).toEqual(200); + expect(res.type).toEqual(expect.stringContaining("json")); + }); + + it("GET /api/status", async () => { + const res = await request.get("/api/status"); + expect(res.status).toEqual(200); + expect(res.type).toEqual(expect.stringContaining("json")); + expect(res.body).toHaveProperty("ApiReachable", true); + }); + + it("GET /api/containers", async () => { + const res = await request.get("/api/containers"); + expect(res.status).toEqual(200); + expect(res.type).toEqual(expect.stringContaining("json")); + }); + + it("GET /api/config", async () => { + const res = await request.get("/api/config"); + expect(res.status).toEqual(200); + expect(res.type).toEqual(expect.stringContaining("json")); + expect(res.body).toHaveProperty("hosts"); + }); + + it("GET /api/current-schedule", async () => { + const res = await request.get("/api/current-schedule"); + + expect(res.status).toEqual(200); + expect(res.type).toEqual(expect.stringContaining("json")); + expect(res.body).toHaveProperty("interval"); + }); + + it("GET /api/frontend-config", async () => { + const res = await request.get("/api/frontend-config"); + + expect(res.status).toEqual(200); + expect(res.type).toEqual(expect.stringContaining("json")); + }); + + it("GET /ha/config", async () => { + const res = await request.get("/ha/config"); + expect(res.status).toEqual(200); + expect(res.type).toEqual(expect.stringContaining("json")); + }); + + it("GET /notification-service/get-template", async () => { + const res = await request.get("/notification-service/get-template"); + + expect(res.status).toEqual(200); + expect(res.type).toEqual(expect.stringContaining("json")); + expect(res.body).toHaveProperty("text"); + }); +}); diff --git a/__tests__/util/previousResponse.ts b/__tests__/util/previousResponse.ts new file mode 100644 index 0000000..774a862 --- /dev/null +++ b/__tests__/util/previousResponse.ts @@ -0,0 +1,23 @@ +let response: string = ""; + +class PreviousResponse { + set(body: unknown): void { + try { + response = JSON.stringify(body).replace(/[" ]/g, ""); + } catch (error: unknown) { + console.error("Error in setting response:", error); + throw new Error("Failed to set response"); + } + } + + get(): string { + try { + return response; + } catch (error: unknown) { + console.error("Error in getting response:", error); + throw new Error("Failed to get response"); + } + } +} + +export const createPreviousResponse = () => new PreviousResponse(); diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index 1f9bf30..76cec4c 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -1,5 +1,5 @@ # Stage 1: Build stage -FROM node:alpine AS builder +FROM node:20-alpine AS builder LABEL maintainer="https://github.com/its4nik" LABEL version="2.0.1" @@ -19,16 +19,14 @@ RUN apk add --no-cache bash COPY tsconfig.json environment.d.ts package*.json ./ -RUN export npm_config_cache=$(mktemp -d) && \ - npm install --production=false && \ - rm -rf $npm_config_cache /tmp/*.log +RUN npm install --production=false COPY ./src ./src RUN mv ./src/sample-variable.json ./src/data/variables.json RUN npm run build:mini # Stage 2: Production stage -FROM node:alpine AS production +FROM node:20-alpine AS production WORKDIR /api @@ -40,10 +38,10 @@ HEALTHCHECK --interval=5m --timeout=3s \ COPY --chown=dockstatapi:dockstatapi --from=builder /app/dist/src /api/src COPY --chown=dockstatapi:dockstatapi --from=builder /app/package*.json /api/ +COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/config/swagger.yaml /api/src/config/swagger.yaml +COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/utils/assets /api/src/utils/assets -RUN export npm_config_cache=$(mktemp -d) && \ - npm install --omit=dev && \ - rm -rf $npm_config_cache /tmp/*.log +RUN npm install --omit=dev COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/misc/entrypoint.sh /api/entrypoint.sh COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/misc/createEnvFile.sh /api/createEnvFile.sh @@ -51,9 +49,10 @@ RUN chmod +x /api/*.sh EXPOSE 9876 -RUN chmod -R 777 /api/src/data /api && \ +RUN mkdir -p /api/src/data && \ + chmod -R 777 /api/src/data /api && \ chown -R dockstatapi:dockstatapi /api STOPSIGNAL 130 USER dockstatapi -ENTRYPOINT [ "bash", "./entrypoint.sh" ] +ENTRYPOINT [ "bash", "./entrypoint.sh", "--prod" ] diff --git a/docker/Dockerfile-dev b/docker/Dockerfile-dev index 58b9f43..43a4240 100644 --- a/docker/Dockerfile-dev +++ b/docker/Dockerfile-dev @@ -1,5 +1,5 @@ # Stage 1: Build stage -FROM node:alpine AS builder +FROM node:20-alpine AS builder LABEL maintainer="https://github.com/its4nik" LABEL version="2.0.1" @@ -19,16 +19,14 @@ RUN apk add --no-cache bash COPY tsconfig.json environment.d.ts package*.json ./ -RUN export npm_config_cache=$(mktemp -d) && \ - npm install --production=false && \ - rm -rf $npm_config_cache /tmp/*.log +RUN npm install --production=false COPY ./src ./src RUN mv ./src/sample-variable.json ./src/data/variables.json RUN npm run build # Stage 2: Production stage -FROM node:alpine AS production +FROM node:20-alpine AS production WORKDIR /api @@ -40,10 +38,10 @@ HEALTHCHECK --interval=5m --timeout=3s \ COPY --chown=dockstatapi:dockstatapi --from=builder /app/dist/src /api/src COPY --chown=dockstatapi:dockstatapi --from=builder /app/package*.json /api/ +COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/config/swagger.yaml /api/src/config/swagger.yaml +COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/utils/assets /api/src/utils/assets -RUN export npm_config_cache=$(mktemp -d) && \ - npm install --omit=dev && \ - rm -rf $npm_config_cache /tmp/*.log +RUN npm install COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/misc/entrypoint.sh /api/entrypoint.sh COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/misc/createEnvFile.sh /api/createEnvFile.sh @@ -51,9 +49,10 @@ RUN chmod +x /api/*.sh EXPOSE 9876 -RUN chmod -R 777 /api/src/data /api && \ +RUN mkdir -p /api/src/data && \ + chmod -R 777 /api/src/data /api && \ chown -R dockstatapi:dockstatapi /api STOPSIGNAL 130 USER dockstatapi -ENTRYPOINT [ "bash", "./entrypoint.sh" ] +ENTRYPOINT [ "bash", "./entrypoint.sh", "--dev" ] diff --git a/docker/docker-compose.dev.yaml b/docker/docker-compose.dev.yaml new file mode 100644 index 0000000..7bc3773 --- /dev/null +++ b/docker/docker-compose.dev.yaml @@ -0,0 +1,40 @@ +services: + test-socket-proxy: + image: lscr.io/linuxserver/socket-proxy:latest + container_name: test-socket-proxy + environment: + - ALLOW_START=1 #optional + - ALLOW_STOP=1 #optional + - ALLOW_RESTARTS=1 #optional + - AUTH=0 #optional + - BUILD=0 #optional + - COMMIT=0 #optional + - CONFIGS=0 #optional + - CONTAINERS=1 #optional + - DISABLE_IPV6=0 #optional + - DISTRIBUTION=0 #optional + - EVENTS=1 #optional + - EXEC=0 #optional + - IMAGES=0 #optional + - INFO=1 #optional + - NETWORKS=1 #optional + - NODES=1 #optional + - PING=1 #optional + - POST=0 #optional + - PLUGINS=0 #optional + - SECRETS=0 #optional + - SERVICES=0 #optional + - SESSION=0 #optional + - SWARM=0 #optional + - SYSTEM=0 #optional + - TASKS=0 #optional + - VERSION=1 #optional + - VOLUMES=0 #optional + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + restart: unless-stopped + read_only: true + tmpfs: + - /run + ports: + - 2375:2375 \ No newline at end of file diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 225c5de..436d8a2 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -79,3 +79,4 @@ services: - /run networks: - shared-network + diff --git a/environment.d.ts b/environment.d.ts index 803ae43..df2595f 100644 --- a/environment.d.ts +++ b/environment.d.ts @@ -2,41 +2,9 @@ declare global { namespace NodeJS { interface ProcessEnv { // Node specific: - NODE_ENV: "development" | "production"; - TRUSTED_PROXYS: string | undefined; - - // User.conf - RUNNING_IN_DOCKER: string | undefined; - VERSION: string | undefined; - - // High Availability - HA_MASTER: string | undefined; //bool - HA_MASTER_IP: string | undefined; - HA_NODE: string | undefined; //ip list with port seperated by "," like: "10.0.0.4:5012,10.0.0.5:9876" - HA_UNSAFE: string | undefined; - - // Notification services: - DISCORD_WEBHOOK_URL: string | undefined; - - EMAIL_SENDER: string | undefined; - EMAIL_RECIPIENT: string | undefined; - EMAIL_PASSWORD: string | undefined; - EMAIL_SERVICE: string | undefined; - - PUSHBULLET_ACCESS_TOKEN: string | undefined; - - PUSHOVER_USER_KEY: string | undefined; - PUSHOVER_API_TOKEN: string | undefined; - - SLACK_WEBHOOK_URL: string | undefined; - - TELEGRAM_BOT_TOKEN: string | undefined; - TELEGRAM_CHAT_ID: string | undefined; - - WHATSAPP_API_URL: string | undefined; - WHATSAPP_RECIPIENT: string | undefined; - - CUSTOM_NOTIFICATION: string | undefined; // enter the script name without .js here and without custom/... + NODE_ENV: "development" | "production" | "testing"; + PORT: string | undefined; + CI: "true" | null; } } } diff --git a/eslint.config.mjs b/eslint.config.mjs index 5b7b70a..56994a6 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -5,7 +5,7 @@ import tseslint from "typescript-eslint"; /** @type {import('eslint').Linter.Config[]} */ export default [ { ignores: ["node_modules/*", "dist/*"] }, - { files: ["src/*.{ts}"] }, + { files: ["src/**/*.ts"] }, { languageOptions: { globals: globals.node } }, pluginJs.configs.recommended, ...tseslint.configs.recommended, diff --git a/nodemon.json b/nodemon.json index 9d946e9..be32c75 100644 --- a/nodemon.json +++ b/nodemon.json @@ -4,7 +4,8 @@ "src/logs", "**/fixtures/**", ".gitignore", - "**/*.json" + "**/*.json", + "**/__tests__/**" ], "execMap": { "ts": "tsx" diff --git a/package-lock.json b/package-lock.json index 8c1ea14..6efc7ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,40 +12,52 @@ "bcrypt": "^5.1.1", "chokidar": "^4.0.1", "cors": "^2.8.5", + "cytoscape": "^3.30.4", + "docker-compose": "^1.1.0", "dockerode": "^4.0.2", "express": "^4.21.1", "express-rate-limit": "^7.4.1", "https": "^1.0.0", + "i": "^0.3.7", "ipaddr.js": "^2.2.0", "nodemailer": "^6.9.16", + "npm": "^11.0.0", + "puppeteer": "^24.0.0", "sqlite3": "^5.1.7", - "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1", "winston": "^3.15.0", - "winston-daily-rotate-file": "^5.0.0" + "winston-daily-rotate-file": "^5.0.0", + "yamljs": "^0.3.0" }, "devDependencies": { "@eslint/js": "^9.17.0", - "@playwright/test": "^1.49.0", "@types/bcrypt": "^5.0.2", "@types/cors": "^2.8.17", + "@types/cytoscape": "^3.21.8", "@types/dockerode": "^3.3.31", "@types/express": "^5.0.0", "@types/express-handlebars": "^5.3.1", + "@types/jest": "^29.5.14", "@types/node": "^22.9.0", + "@types/node-fetch": "^2.6.12", "@types/nodemailer": "^6.4.17", + "@types/supertest": "^6.0.2", "@types/supports-color": "^8.1.3", "@types/swagger-jsdoc": "^6.0.4", "@types/swagger-ui-express": "^4.1.7", + "@types/ws": "^8.5.14", + "@types/yamljs": "^0.2.34", "@typescript-eslint/eslint-plugin": "^8.18.2", "@typescript-eslint/parser": "^8.18.2", "dependency-cruiser": "^16.5.0", "eslint": "^9.17.0", "globals": "^15.14.0", + "jest": "^29.7.0", "license-checker": "^25.0.1", "nodemon": "^3.1.7", - "ora": "^8.1.1", "prettier": "^3.4.2", + "supertest": "^7.0.0", + "ts-jest": "^29.2.5", "ts-node": "^10.9.2", "tsx": "^4.19.2", "typescript-eslint": "^8.18.2", @@ -55,314 +67,588 @@ "npm": ">=10.8.2" } }, - "node_modules/@apidevtools/json-schema-ref-parser": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz", - "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==", - "license": "MIT", + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@jsdevtools/ono": "^7.1.3", - "@types/json-schema": "^7.0.6", - "call-me-maybe": "^1.0.1", - "js-yaml": "^4.1.0" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" } }, - "node_modules/@apidevtools/openapi-schemas": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", - "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, "engines": { - "node": ">=10" + "node": ">=6.9.0" } }, - "node_modules/@apidevtools/swagger-methods": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", - "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", - "license": "MIT" + "node_modules/@babel/compat-data": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.5.tgz", + "integrity": "sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } }, - "node_modules/@apidevtools/swagger-parser": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz", - "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==", + "node_modules/@babel/core": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.7.tgz", + "integrity": "sha512-SRijHmF0PSPgLIBYlWnG0hyeJLwXE2CgpsXaMOrtt2yp9/86ALw6oUlj9KYuZ0JN07T4eBMVIW4li/9S1j2BGA==", + "dev": true, "license": "MIT", "dependencies": { - "@apidevtools/json-schema-ref-parser": "^9.0.6", - "@apidevtools/openapi-schemas": "^2.0.4", - "@apidevtools/swagger-methods": "^3.0.2", - "@jsdevtools/ono": "^7.1.3", - "call-me-maybe": "^1.0.1", - "z-schema": "^5.0.1" + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.5", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.7", + "@babel/parser": "^7.26.7", + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.26.7", + "@babel/types": "^7.26.7", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" }, - "peerDependencies": { - "openapi-types": ">=7" + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "node_modules/@balena/dockerignore": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz", - "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==", - "license": "Apache-2.0" + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } }, - "node_modules/@colors/colors": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", - "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "node_modules/@babel/generator": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz", + "integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==", + "dev": true, "license": "MIT", + "dependencies": { + "@babel/parser": "^7.26.5", + "@babel/types": "^7.26.5", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, "engines": { - "node": ">=0.1.90" + "node": ">=6.9.0" } }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "node_modules/@babel/helper-compilation-targets": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", + "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" + "@babel/compat-data": "^7.26.5", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" }, "engines": { - "node": ">=12" + "node": ">=6.9.0" } }, - "node_modules/@dabh/diagnostics": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", - "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "dev": true, "license": "MIT", "dependencies": { - "colorspace": "1.1.x", - "enabled": "2.0.x", - "kuler": "^2.0.0" + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", - "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", - "cpu": [ - "ppc64" - ], + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "aix" - ], + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", - "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", - "cpu": [ - "arm" - ], + "node_modules/@babel/helper-plugin-utils": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", - "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", - "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "license": "MIT", - "optional": true, - "os": [ - "android" - ], "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", - "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", - "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", - "cpu": [ - "x64" - ], + "node_modules/@babel/helpers": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.7.tgz", + "integrity": "sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.7" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", - "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/parser": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.7.tgz", + "integrity": "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "dependencies": { + "@babel/types": "^7.26.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, "engines": { - "node": ">=18" + "node": ">=6.0.0" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", - "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", - "cpu": [ - "x64" - ], + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", - "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", - "cpu": [ - "arm" - ], + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", - "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", - "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", - "cpu": [ - "ia32" - ], + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", - "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", - "cpu": [ - "loong64" - ], + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", + "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", - "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", - "cpu": [ - "mips64el" - ], + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", + "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/linux-ppc64": { + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", + "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.7.tgz", + "integrity": "sha512-1x1sgeyRLC3r5fQOM0/xtQKsYjyxmFjaOrLJNtZ81inNjyJHGIolTULPiSc/2qe1/qfpFLisLQYFnnZl7QoedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.5", + "@babel/parser": "^7.26.7", + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.7.tgz", + "integrity": "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@balena/dockerignore": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz", + "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==", + "license": "Apache-2.0" + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "license": "MIT", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", - "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", + "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", "cpu": [ "ppc64" ], @@ -370,50 +656,50 @@ "license": "MIT", "optional": true, "os": [ - "linux" + "aix" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/linux-riscv64": { + "node_modules/@esbuild/android-arm": { "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", - "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", + "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", "cpu": [ - "riscv64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "android" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/linux-s390x": { + "node_modules/@esbuild/android-arm64": { "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", - "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", + "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", "cpu": [ - "s390x" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "android" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/linux-x64": { + "node_modules/@esbuild/android-x64": { "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", - "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", + "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", "cpu": [ "x64" ], @@ -421,67 +707,67 @@ "license": "MIT", "optional": true, "os": [ - "linux" + "android" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/netbsd-x64": { + "node_modules/@esbuild/darwin-arm64": { "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", - "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", + "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "netbsd" + "darwin" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/openbsd-arm64": { + "node_modules/@esbuild/darwin-x64": { "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", - "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", + "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "openbsd" + "darwin" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/openbsd-x64": { + "node_modules/@esbuild/freebsd-arm64": { "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", - "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", + "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "openbsd" + "freebsd" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/sunos-x64": { + "node_modules/@esbuild/freebsd-x64": { "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", - "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", + "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", "cpu": [ "x64" ], @@ -489,32 +775,253 @@ "license": "MIT", "optional": true, "os": [ - "sunos" + "freebsd" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/win32-arm64": { + "node_modules/@esbuild/linux-arm": { "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", - "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", + "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", "cpu": [ - "arm64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" + "linux" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/win32-ia32": { + "node_modules/@esbuild/linux-arm64": { "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", + "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", + "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", + "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", + "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", + "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", + "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", + "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", + "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", + "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", + "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", + "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", + "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", + "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", "cpu": [ "ia32" @@ -576,13 +1083,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz", - "integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", + "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.5", + "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -615,9 +1122,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.1.tgz", - "integrity": "sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q==", + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.11.0.tgz", + "integrity": "sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -713,9 +1220,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.17.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.17.0.tgz", - "integrity": "sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==", + "version": "9.20.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.20.0.tgz", + "integrity": "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==", "dev": true, "license": "MIT", "engines": { @@ -723,9 +1230,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz", - "integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -733,18 +1240,32 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.4.tgz", - "integrity": "sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==", + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz", + "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==", "dev": true, "license": "Apache-2.0", "dependencies": { + "@eslint/core": "^0.10.0", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", + "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -752,6 +1273,37 @@ "license": "MIT", "optional": true }, + "node_modules/@grpc/grpc-js": { + "version": "1.12.6", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.12.6.tgz", + "integrity": "sha512-JXUj6PI0oqqzTGvKtzOkxtpsyPRNsrmhh41TtIz/zEB6J+AUiZZ0dxWzcMwO9Ns5rmSPuMdghlTbUuqIM48d3Q==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.7.13", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", + "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -818,14 +1370,455 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" } }, "node_modules/@jridgewell/sourcemap-codec": { @@ -836,21 +1829,25 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@jsdevtools/ono": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", - "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", - "license": "MIT" + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } }, "node_modules/@mapbox/node-pre-gyp": { "version": "1.0.11", @@ -949,40 +1946,167 @@ "node": ">=10" } }, - "node_modules/@playwright/test": { - "version": "1.49.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.1.tgz", - "integrity": "sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright": "1.49.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@scarf/scarf": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", - "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", - "hasInstallScript": true, - "license": "Apache-2.0" + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" }, - "node_modules/@tootallnate/once": { + "node_modules/@protobufjs/base64": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 6" - } + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" }, - "node_modules/@tsconfig/node10": { + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@puppeteer/browsers": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.7.1.tgz", + "integrity": "sha512-MK7rtm8JjaxPN7Mf1JdZIZKPD2Z+W7osvrC1vjpvfOX1K0awDIHYbNi89f7eotp7eMUn2shWnt03HwVbriXtKQ==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.0", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.0", + "tar-fs": "^3.0.8", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@puppeteer/browsers/node_modules/tar-fs": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.8.tgz", + "integrity": "sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/@puppeteer/browsers/node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", @@ -1010,6 +2134,51 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, "node_modules/@types/bcrypt": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", @@ -1041,6 +2210,13 @@ "@types/node": "*" } }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/cors": { "version": "2.8.17", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", @@ -1051,6 +2227,13 @@ "@types/node": "*" } }, + "node_modules/@types/cytoscape": { + "version": "3.21.9", + "resolved": "https://registry.npmjs.org/@types/cytoscape/-/cytoscape-3.21.9.tgz", + "integrity": "sha512-JyrG4tllI6jvuISPjHK9j2Xv/LTbnLekLke5otGStjFluIyA9JjgnvgZrSBsp8cEDpiTjwgZUZwpPv8TSBcoLw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/docker-modem": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz", @@ -1063,9 +2246,9 @@ } }, "node_modules/@types/dockerode": { - "version": "3.3.32", - "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.32.tgz", - "integrity": "sha512-xxcG0g5AWKtNyh7I7wswLdFvym4Mlqks5ZlKzxEUrGHS0r0PUOfxm2T0mspwu10mHQqu3Ck3MI3V2HqvLWE1fg==", + "version": "3.3.34", + "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.34.tgz", + "integrity": "sha512-mH9SuIb8NuTDsMus5epcbTzSbEo52fKLBMo0zapzYIAIyfDqoIFn7L3trekHLKC8qmxGV++pPUP4YqQ9n5v2Zg==", "dev": true, "license": "MIT", "dependencies": { @@ -1102,9 +2285,9 @@ "license": "MIT" }, "node_modules/@types/express-serve-static-core": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.3.tgz", - "integrity": "sha512-JEhMNwUJt7bw728CydvYzntD0XJeTmDnvwLlbfbAhE7Tbslm/ax6bdIiUwTgeVlZTsJQPwZwKpAkyDtIjsvx3g==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", + "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", "dev": true, "license": "MIT", "dependencies": { @@ -1114,6 +2297,16 @@ "@types/send": "*" } }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", @@ -1121,10 +2314,56 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, "license": "MIT" }, "node_modules/@types/mime": { @@ -1135,15 +2374,25 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.10.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.3.tgz", - "integrity": "sha512-DifAyw4BkrufCILvD3ucnuN8eydUfc/C1GlyrnI+LK6543w5/L3VeVgf05o3B4fqSXP1dKYLOZsKfutpxPzZrw==", - "dev": true, + "version": "22.13.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.1.tgz", + "integrity": "sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==", "license": "MIT", "dependencies": { "undici-types": "~6.20.0" } }, + "node_modules/@types/node-fetch": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, "node_modules/@types/nodemailer": { "version": "6.4.17", "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz", @@ -1155,9 +2404,9 @@ } }, "node_modules/@types/qs": { - "version": "6.9.17", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz", - "integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==", + "version": "6.9.18", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", + "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", "dev": true, "license": "MIT" }, @@ -1192,9 +2441,9 @@ } }, "node_modules/@types/ssh2": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.1.tgz", - "integrity": "sha512-ZIbEqKAsi5gj35y4P4vkJYly642wIbY6PqoN0xiyQGshKUGXR9WQjF/iF9mXBQ8uBKy3ezfsCkcoHKhd0BzuDA==", + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.4.tgz", + "integrity": "sha512-9JTQgVBWSgq6mAen6PVnrAmty1lqgCMvpfN+1Ck5WRUsyMYPa6qd50/vMJ0y1zkGpOEgLzm8m8Dx/Y5vRouLaA==", "dev": true, "license": "MIT", "dependencies": { @@ -1202,9 +2451,9 @@ } }, "node_modules/@types/ssh2/node_modules/@types/node": { - "version": "18.19.69", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.69.tgz", - "integrity": "sha512-ECPdY1nlaiO/Y6GUnwgtAAhLNaQ53AyIVz+eILxpEo5OvuqE6yWkqWBIb5dU0DqhKQtMeny+FBD3PK6lm7L5xQ==", + "version": "18.19.75", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.75.tgz", + "integrity": "sha512-UIksWtThob6ZVSyxcOqCLOUNg/dyO1Qvx4McgeuhrEtHTLFTf7BBhEazaE4K806FGTPtzd/2sE90qn4fVr7cyw==", "dev": true, "license": "MIT", "dependencies": { @@ -1218,6 +2467,37 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/superagent": { + "version": "8.1.9", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", + "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.2.tgz", + "integrity": "sha512-137ypx2lk/wTQbW6An6safu9hXmajAifU/s7szAHLN/FeIm5w7yR0Wkl9fdJMRSHwOn4HLAI0DaB2TOORuhPDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, "node_modules/@types/supports-color": { "version": "8.1.3", "resolved": "https://registry.npmjs.org/@types/supports-color/-/supports-color-8.1.3.tgz", @@ -1249,22 +2529,66 @@ "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", "license": "MIT" }, + "node_modules/@types/ws": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.14.tgz", + "integrity": "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yamljs": { + "version": "0.2.34", + "resolved": "https://registry.npmjs.org/@types/yamljs/-/yamljs-0.2.34.tgz", + "integrity": "sha512-gJvfRlv9ErxdOv7ux7UsJVePtX54NAvQyd8ncoiFqK8G5aeHIfQfGH2fbruvjAQ9657HwAaO54waS+Dsk2QTUQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.19.0.tgz", - "integrity": "sha512-NggSaEZCdSrFddbctrVjkVZvFC6KGfKfNK0CU7mNK/iKHGKbzT4Wmgm08dKpcZECBu9f5FypndoMyRHkdqfT1Q==", + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.23.0.tgz", + "integrity": "sha512-vBz65tJgRrA1Q5gWlRfvoH+w943dq9K1p1yDBY2pc+a1nbBLZp7fB9+Hk8DaALUbzjqlMfgaqlVPT1REJdkt/w==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.19.0", - "@typescript-eslint/type-utils": "8.19.0", - "@typescript-eslint/utils": "8.19.0", - "@typescript-eslint/visitor-keys": "8.19.0", + "@typescript-eslint/scope-manager": "8.23.0", + "@typescript-eslint/type-utils": "8.23.0", + "@typescript-eslint/utils": "8.23.0", + "@typescript-eslint/visitor-keys": "8.23.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1280,16 +2604,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.19.0.tgz", - "integrity": "sha512-6M8taKyOETY1TKHp0x8ndycipTVgmp4xtg5QpEZzXxDhNvvHOJi5rLRkLr8SK3jTgD5l4fTlvBiRdfsuWydxBw==", + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.23.0.tgz", + "integrity": "sha512-h2lUByouOXFAlMec2mILeELUbME5SZRN/7R9Cw2RD2lRQQY08MWMM+PmVVKKJNK1aIwqTo9t/0CvOxwPbRIE2Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.19.0", - "@typescript-eslint/types": "8.19.0", - "@typescript-eslint/typescript-estree": "8.19.0", - "@typescript-eslint/visitor-keys": "8.19.0", + "@typescript-eslint/scope-manager": "8.23.0", + "@typescript-eslint/types": "8.23.0", + "@typescript-eslint/typescript-estree": "8.23.0", + "@typescript-eslint/visitor-keys": "8.23.0", "debug": "^4.3.4" }, "engines": { @@ -1305,14 +2629,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.19.0.tgz", - "integrity": "sha512-hkoJiKQS3GQ13TSMEiuNmSCvhz7ujyqD1x3ShbaETATHrck+9RaDdUbt+osXaUuns9OFwrDTTrjtwsU8gJyyRA==", + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.23.0.tgz", + "integrity": "sha512-OGqo7+dXHqI7Hfm+WqkZjKjsiRtFUQHPdGMXzk5mYXhJUedO7e/Y7i8AK3MyLMgZR93TX4bIzYrfyVjLC+0VSw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.19.0", - "@typescript-eslint/visitor-keys": "8.19.0" + "@typescript-eslint/types": "8.23.0", + "@typescript-eslint/visitor-keys": "8.23.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1323,16 +2647,16 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.19.0.tgz", - "integrity": "sha512-TZs0I0OSbd5Aza4qAMpp1cdCYVnER94IziudE3JU328YUHgWu9gwiwhag+fuLeJ2LkWLXI+F/182TbG+JaBdTg==", + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.23.0.tgz", + "integrity": "sha512-iIuLdYpQWZKbiH+RkCGc6iu+VwscP5rCtQ1lyQ7TYuKLrcZoeJVpcLiG8DliXVkUxirW/PWlmS+d6yD51L9jvA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.19.0", - "@typescript-eslint/utils": "8.19.0", + "@typescript-eslint/typescript-estree": "8.23.0", + "@typescript-eslint/utils": "8.23.0", "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1347,9 +2671,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.19.0.tgz", - "integrity": "sha512-8XQ4Ss7G9WX8oaYvD4OOLCjIQYgRQxO+qCiR2V2s2GxI9AUpo7riNwo6jDhKtTcaJjT8PY54j2Yb33kWtSJsmA==", + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.23.0.tgz", + "integrity": "sha512-1sK4ILJbCmZOTt9k4vkoulT6/y5CHJ1qUYxqpF1K/DBAd8+ZUL4LlSCxOssuH5m4rUaaN0uS0HlVPvd45zjduQ==", "dev": true, "license": "MIT", "engines": { @@ -1361,20 +2685,20 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.19.0.tgz", - "integrity": "sha512-WW9PpDaLIFW9LCbucMSdYUuGeFUz1OkWYS/5fwZwTA+l2RwlWFdJvReQqMUMBw4yJWJOfqd7An9uwut2Oj8sLw==", + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.23.0.tgz", + "integrity": "sha512-LcqzfipsB8RTvH8FX24W4UUFk1bl+0yTOf9ZA08XngFwMg4Kj8A+9hwz8Cr/ZS4KwHrmo9PJiLZkOt49vPnuvQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.19.0", - "@typescript-eslint/visitor-keys": "8.19.0", + "@typescript-eslint/types": "8.23.0", + "@typescript-eslint/visitor-keys": "8.23.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1388,16 +2712,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.19.0.tgz", - "integrity": "sha512-PTBG+0oEMPH9jCZlfg07LCB2nYI0I317yyvXGfxnvGvw4SHIOuRnQ3kadyyXY6tGdChusIHIbM5zfIbp4M6tCg==", + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.23.0.tgz", + "integrity": "sha512-uB/+PSo6Exu02b5ZEiVtmY6RVYO7YU5xqgzTIVZwTHvvK3HsL8tZZHFaTLFtRG3CsV4A5mhOv+NZx5BlhXPyIA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.19.0", - "@typescript-eslint/types": "8.19.0", - "@typescript-eslint/typescript-estree": "8.19.0" + "@typescript-eslint/scope-manager": "8.23.0", + "@typescript-eslint/types": "8.23.0", + "@typescript-eslint/typescript-estree": "8.23.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1412,13 +2736,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.19.0.tgz", - "integrity": "sha512-mCFtBbFBJDCNCWUl5y6sZSCHXw1DEFEk3c/M3nRK2a4XUB8StGFtmcEMizdjKuBzB6e/smJAAWYug3VrdLMr1w==", + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.23.0.tgz", + "integrity": "sha512-oWWhcWDLwDfu++BGTZcmXWqpwtkwb5o7fxUIGksMQQDSdPW9prsSnfIOZMlsj4vBOSrcnjIUZMiIjODgGosFhQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.19.0", + "@typescript-eslint/types": "8.23.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -1573,6 +2897,22 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -1586,7 +2926,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -1690,65 +3029,289 @@ "safer-buffer": "~2.1.0" } }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "license": "MIT" }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, "license": "MIT" }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" + "node_modules/b4a": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "license": "Apache-2.0" }, - "node_modules/bcrypt": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", - "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", - "hasInstallScript": true, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, "license": "MIT", "dependencies": { - "@mapbox/node-pre-gyp": "^1.0.11", - "node-addon-api": "^5.0.0" + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" }, "engines": { - "node": ">= 10.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" } }, - "node_modules/bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { - "tweetnacl": "^0.14.3" + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/bare-events": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz", + "integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/bare-fs": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.0.1.tgz", + "integrity": "sha512-ilQs4fm/l9eMfWY2dY0WCIUplSUp7U0CT1vrqMg1MUdeZl4fypu5UP0XcDBK5WBQPJAKP1b7XEodISmekH/CEg==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-events": "^2.0.0", + "bare-path": "^3.0.0", + "bare-stream": "^2.0.0" + }, + "engines": { + "bare": ">=1.7.0" + } + }, + "node_modules/bare-os": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.4.0.tgz", + "integrity": "sha512-9Ous7UlnKbe3fMi7Y+qh0DwAup6A1JkYgPnjvMDNOlmnxNRQvQ/7Nst+OnUQKzk0iAT0m9BisbDVp9gCv8+ETA==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "bare": ">=1.6.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz", + "integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "streamx": "^2.21.0" + }, + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, "license": "MIT", "engines": { @@ -1840,6 +3403,62 @@ "node": ">=8" } }, + "node_modules/browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -1864,6 +3483,22 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, "node_modules/buildcheck": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", @@ -1912,6 +3547,19 @@ "node": ">= 10" } }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/cacache/node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -1925,6 +3573,13 @@ "node": ">=10" } }, + "node_modules/cacache/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", @@ -1954,22 +3609,46 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/call-me-maybe": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", - "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", - "license": "MIT" - }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/caniuse-lite": { + "version": "1.0.30001698", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001698.tgz", + "integrity": "sha512-xJ3km2oiG/MbNU8G6zIq6XRZ6HtAOVXsbOrP/blGazi52kc5Yy7b6sDA5O+FbROzRrV7BSTllLHuNvmawYUJjw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1987,6 +3666,16 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -2011,6 +3700,42 @@ "node": ">=10" } }, + "node_modules/chromium-bidi": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-1.2.0.tgz", + "integrity": "sha512-XtdJ1GSN6S3l7tO7F77GhNsw0K367p0IsLYf2yZawCVAKKC3lUvDhPdMVrB2FNhmhfW43QGYbEX3Wg6q0maGwQ==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -2021,35 +3746,38 @@ "node": ">=6" } }, - "node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "dev": true, - "license": "MIT", + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", "dependencies": { - "restore-cursor": "^5.0.0" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=12" } }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" } }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "license": "MIT" + }, "node_modules/color": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", @@ -2064,7 +3792,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -2123,16 +3850,39 @@ "text-hex": "1.0.x" } }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", "dev": true, "license": "MIT", "engines": { "node": ">=18" } }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2166,6 +3916,13 @@ "node": ">= 0.6" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cookie": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", @@ -2181,6 +3938,13 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -2194,6 +3958,32 @@ "node": ">= 0.10" } }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/cpu-features": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", @@ -2208,6 +3998,28 @@ "node": ">=10.0.0" } }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -2230,6 +4042,24 @@ "node": ">= 8" } }, + "node_modules/cytoscape": { + "version": "3.31.0", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.31.0.tgz", + "integrity": "sha512-zDGn1K/tfZwEnoGOcHc0H4XazqAAXAuDpcYw9mUnUjATjqljyCNGJv8uEvbvxGaGHaVshxMecyl6oc6uKzRfbw==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -2273,6 +4103,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -2289,6 +4134,40 @@ "dev": true, "license": "MIT" }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -2305,9 +4184,9 @@ } }, "node_modules/dependency-cruiser": { - "version": "16.8.0", - "resolved": "https://registry.npmjs.org/dependency-cruiser/-/dependency-cruiser-16.8.0.tgz", - "integrity": "sha512-VyBzIrLHfG7rT36URln+CTy8VSjrLB7YDlMx5vtBSHRHCOXgLUCcP4n5ZoD+s166T0i5LN33q1CvBkEOGsDTSg==", + "version": "16.9.0", + "resolved": "https://registry.npmjs.org/dependency-cruiser/-/dependency-cruiser-16.9.0.tgz", + "integrity": "sha512-Gc/xHNOBq1nk5i7FPCuexCD0m2OXB/WEfiSHfNYQaQaHZiZltnl5Ixp/ZG38Jvi8aEhKBQTHV4Aw6gmR7rWlOw==", "dev": true, "license": "MIT", "dependencies": { @@ -2317,9 +4196,9 @@ "acorn-loose": "^8.4.0", "acorn-walk": "^8.3.4", "ajv": "^8.17.1", - "commander": "^12.1.0", - "enhanced-resolve": "^5.17.1", - "ignore": "^6.0.2", + "commander": "^13.0.0", + "enhanced-resolve": "^5.18.0", + "ignore": "^7.0.0", "interpret": "^3.1.1", "is-installed-globally": "^1.0.0", "json5": "^2.2.3", @@ -2347,9 +4226,9 @@ } }, "node_modules/dependency-cruiser/node_modules/ignore": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-6.0.2.tgz", - "integrity": "sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.3.tgz", + "integrity": "sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA==", "dev": true, "license": "MIT", "engines": { @@ -2375,6 +4254,22 @@ "node": ">=8" } }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1402036", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1402036.tgz", + "integrity": "sha512-JwAYQgEvm3yD45CHB+RmF5kMbWtXBaOGwuxa87sZogHcLCv8c/IqnThaoQ1y60d7pXWjSKWQphPEc+1rAScVdg==", + "license": "BSD-3-Clause" + }, "node_modules/dezalgo": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", @@ -2396,10 +4291,32 @@ "node": ">=0.3.1" } }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/docker-compose": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/docker-compose/-/docker-compose-1.1.0.tgz", + "integrity": "sha512-VrkQJNafPQ5d6bGULW0P6KqcxSkv3ZU5Wn2wQA19oB71o7+55vQ9ogFe2MMeNbK+jc9rrKVy280DnHO5JLMWOQ==", + "license": "MIT", + "dependencies": { + "yaml": "^2.2.2" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/docker-modem": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.3.tgz", - "integrity": "sha512-89zhop5YVhcPEt5FpUFGr3cDyceGhq/F9J+ZndQ4KfqNvfbJpPMfgeixFgUj5OjCYAboElqODxY5Z1EBsSa6sg==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.6.tgz", + "integrity": "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ==", "license": "Apache-2.0", "dependencies": { "debug": "^4.1.1", @@ -2412,31 +4329,23 @@ } }, "node_modules/dockerode": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.2.tgz", - "integrity": "sha512-9wM1BVpVMFr2Pw3eJNXrYYt6DT9k0xMcsSCjtPvyQ+xa1iPg/Mo3T/gUcwI0B2cczqCeCYRPF8yFYDwtFXT0+w==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.4.tgz", + "integrity": "sha512-6GYP/EdzEY50HaOxTVTJ2p+mB5xDHTMJhS+UoGrVyS6VC+iQRh7kZ4FRpUYq6nziby7hPqWhOrFFUFTMUZJJ5w==", "license": "Apache-2.0", "dependencies": { "@balena/dockerignore": "^1.0.2", - "docker-modem": "^5.0.3", - "tar-fs": "~2.0.1" + "@grpc/grpc-js": "^1.11.1", + "@grpc/proto-loader": "^0.7.13", + "docker-modem": "^5.0.6", + "protobufjs": "^7.3.2", + "tar-fs": "~2.0.1", + "uuid": "^10.0.0" }, "engines": { "node": ">= 8.0" } }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2457,6 +4366,42 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.96", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.96.tgz", + "integrity": "sha512-8AJUW6dh75Fm/ny8+kZKJzI1pgoE8bKLZlzDU2W1ENd+DXKJrx7I7l9hb8UWR4ojlnb5OlixMt00QWiYJoVw1w==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -2511,9 +4456,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.18.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz", - "integrity": "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==", + "version": "5.18.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", + "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", "dev": true, "license": "MIT", "dependencies": { @@ -2529,7 +4474,6 @@ "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", "license": "MIT", - "optional": true, "engines": { "node": ">=6" } @@ -2541,6 +4485,15 @@ "license": "MIT", "optional": true }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -2560,9 +4513,9 @@ } }, "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -2611,6 +4564,15 @@ "@esbuild/win32-x64": "0.23.1" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -2630,20 +4592,41 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, "node_modules/eslint": { - "version": "9.17.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.17.0.tgz", - "integrity": "sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA==", + "version": "9.20.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.20.0.tgz", + "integrity": "sha512-aL4F8167Hg4IvsW89ejnpTwx+B/UQRzJPGgbIOl+4XqffWsahVVsLEWoZvnrVuwpWmnRd7XeXmQI1zlKcFDteA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.19.0", - "@eslint/core": "^0.9.0", + "@eslint/core": "^0.11.0", "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.17.0", - "@eslint/plugin-kit": "^0.2.3", + "@eslint/js": "9.20.0", + "@eslint/plugin-kit": "^0.2.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.1", @@ -2812,6 +4795,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", @@ -2842,7 +4838,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -2866,6 +4861,39 @@ "node": ">= 0.6" } }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -2875,6 +4903,23 @@ "node": ">=6" } }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/express": { "version": "4.21.2", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", @@ -2951,6 +4996,41 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2958,10 +5038,16 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, "license": "MIT", "dependencies": { @@ -2969,7 +5055,7 @@ "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "micromatch": "^4.0.8" }, "engines": { "node": ">=8.6.0" @@ -3002,28 +5088,64 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-uri": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", - "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], "license": "BSD-3-Clause" }, "node_modules/fastq": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", - "integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz", + "integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==", "dev": true, "license": "ISC", "dependencies": { "reusify": "^1.0.4" } }, - "node_modules/fecha": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", - "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", - "license": "MIT" + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" }, "node_modules/file-entry-cache": { "version": "8.0.0", @@ -3053,6 +5175,29 @@ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", "license": "MIT" }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -3143,6 +5288,36 @@ "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", "license": "MIT" }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formidable": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.2.tgz", + "integrity": "sha512-Jqc1btCy3QzRbJaICGwKcBfGWuLADRerLzDqi2NwSt/UkXLsHJw2TVResiaoBufHVHy9aSgClOHCeJsSsFLTbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dezalgo": "^1.0.4", + "hexoid": "^2.0.0", + "once": "^1.4.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -3186,9 +5361,9 @@ "license": "ISC" }, "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -3230,17 +5405,23 @@ "node": ">=10" } }, - "node_modules/get-east-asian-width": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", - "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, "license": "MIT", "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" } }, "node_modules/get-intrinsic": { @@ -3267,10 +5448,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/get-proto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.0.tgz", - "integrity": "sha512-TtLgOcKaF1nMP2ijJnITkE4nRhbpshHhmzKiuhmSniiwWzovoqwqQ8rNuhf0mXJOqIY5iU+QkUe0CkJYrLsG9w==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -3280,10 +5471,23 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-tsconfig": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz", - "integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==", + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", + "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", "dev": true, "license": "MIT", "dependencies": { @@ -3293,6 +5497,20 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/get-uri": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.4.tgz", + "integrity": "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -3450,6 +5668,16 @@ "node": ">= 0.4" } }, + "node_modules/hexoid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-2.0.0.tgz", + "integrity": "sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", @@ -3457,6 +5685,13 @@ "dev": true, "license": "ISC" }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/http-cache-semantics": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", @@ -3481,18 +5716,25 @@ } }, "node_modules/http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "license": "MIT", - "optional": true, "dependencies": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" + "agent-base": "^7.1.0", + "debug": "^4.3.4" }, "engines": { - "node": ">= 6" + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "license": "MIT", + "engines": { + "node": ">= 14" } }, "node_modules/https": { @@ -3514,6 +5756,16 @@ "node": ">= 6" } }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, "node_modules/humanize-ms": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", @@ -3524,6 +5776,14 @@ "ms": "^2.0.0" } }, + "node_modules/i": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/i/-/i-0.3.7.tgz", + "integrity": "sha512-FYz4wlXgkQwIPqhzC5TdNMLSE5+GS1IIDJZY/1ZiEPCT2S3COUVZeT5OW4BmW4r5LHLQuOosSwsvnroG9GR59Q==", + "engines": { + "node": ">=0.4" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -3574,10 +5834,9 @@ "license": "ISC" }, "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -3590,6 +5849,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -3659,7 +5938,6 @@ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", "license": "MIT", - "optional": true, "dependencies": { "jsbn": "1.1.0", "sprintf-js": "^1.1.3" @@ -3678,9 +5956,9 @@ } }, "node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "license": "MIT" }, "node_modules/is-binary-path": { @@ -3731,6 +6009,16 @@ "node": ">=8" } }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -3761,19 +6049,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-interactive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", - "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-lambda": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", @@ -3816,19 +6091,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-unicode-supported": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", - "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3836,490 +6098,637 @@ "devOptional": true, "license": "ISC" }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "license": "MIT", + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "argparse": "^2.0.1" + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "engines": { + "node": ">=10" } }, - "node_modules/jsbn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", - "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", - "license": "MIT", - "optional": true - }, - "node_modules/json-buffer": { + "node_modules/istanbul-lib-report": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, - "license": "MIT" - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=6" + "node": ">=10" } }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, "license": "MIT", "dependencies": { - "json-buffer": "3.0.1" + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, "engines": { - "node": ">=6" + "node": ">=10" } }, - "node_modules/kuler": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", - "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", - "license": "MIT" - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" }, "engines": { - "node": ">= 0.8.0" + "node": ">=8" } }, - "node_modules/license-checker": { - "version": "25.0.1", - "resolved": "https://registry.npmjs.org/license-checker/-/license-checker-25.0.1.tgz", - "integrity": "sha512-mET5AIwl7MR2IAKYYoVBBpV0OnkKQ1xGj2IMMeEFIs42QAkEVjRtFZGWmQ28WeU7MP779iAgOaOy93Mn44mn6g==", + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", "dev": true, - "license": "BSD-3-Clause", + "license": "Apache-2.0", "dependencies": { - "chalk": "^2.4.1", - "debug": "^3.1.0", - "mkdirp": "^0.5.1", - "nopt": "^4.0.1", - "read-installed": "~4.0.3", - "semver": "^5.5.0", - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0", - "spdx-satisfies": "^4.0.0", - "treeify": "^1.1.0" + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" }, "bin": { - "license-checker": "bin/license-checker" + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" } }, - "node_modules/license-checker/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "node_modules/jake/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^1.9.0" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jake/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=4" + "node": "*" } }, - "node_modules/license-checker/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" }, "engines": { - "node": ">=4" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/license-checker/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "1.1.3" + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/license-checker/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true, - "license": "MIT" - }, - "node_modules/license-checker/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", "dev": true, "license": "MIT", "dependencies": { - "ms": "^2.1.1" + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/license-checker/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", "dev": true, "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, "engines": { - "node": ">=0.8.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/license-checker/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, "engines": { - "node": ">=4" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } } }, - "node_modules/license-checker/node_modules/nopt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", - "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "abbrev": "1", - "osenv": "^0.1.4" + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, - "bin": { - "nopt": "bin/nopt.js" + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/license-checker/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/license-checker/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", "dev": true, "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" }, "engines": { - "node": ">=4" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", "dev": true, "license": "MIT", "dependencies": { - "p-locate": "^5.0.0" + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", - "license": "MIT" - }, - "node_modules/lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", - "license": "MIT" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", "dev": true, - "license": "MIT" - }, - "node_modules/lodash.mergewith": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", - "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", - "license": "MIT" + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } }, - "node_modules/log-symbols": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", - "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", "dev": true, "license": "MIT", "dependencies": { - "chalk": "^5.3.0", - "is-unicode-supported": "^1.3.0" + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" }, "engines": { - "node": ">=18" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "optionalDependencies": { + "fsevents": "^2.3.2" } }, - "node_modules/log-symbols/node_modules/chalk": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", "dev": true, "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/log-symbols/node_modules/is-unicode-supported": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", - "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", "dev": true, "license": "MIT", - "engines": { - "node": ">=12" + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/logform": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", - "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, "license": "MIT", "dependencies": { - "@colors/colors": "1.6.0", - "@types/triple-beam": "^1.3.2", - "fecha": "^4.2.0", - "ms": "^2.1.1", - "safe-stable-stringify": "^2.3.1", - "triple-beam": "^1.3.0" + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" }, "engines": { - "node": ">= 12.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "license": "ISC", - "optional": true, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", "dependencies": { - "yallist": "^4.0.0" + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" }, "engines": { - "node": ">=10" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, "license": "MIT", - "dependencies": { - "semver": "^6.0.0" - }, "engines": { - "node": ">=8" + "node": ">=6" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } } }, - "node_modules/make-dir/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", "dev": true, - "license": "ISC" - }, - "node_modules/make-fetch-happen": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", - "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", - "license": "ISC", - "optional": true, - "dependencies": { - "agentkeepalive": "^4.1.3", - "cacache": "^15.2.0", - "http-cache-semantics": "^4.1.0", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^6.0.0", - "minipass": "^3.1.3", - "minipass-collect": "^1.0.2", - "minipass-fetch": "^1.3.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.2", - "promise-retry": "^2.0.1", - "socks-proxy-agent": "^6.0.0", - "ssri": "^8.0.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/memoize": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/memoize/-/memoize-10.0.0.tgz", - "integrity": "sha512-H6cBLgsi6vMWOcCpvVCdFFnl3kerEXbrYh9q+lY6VXvQSmM6CkmV08VOwT+WE2tzIEqRPFfAq3fm4v/UIW6mSA==", + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", "dev": true, "license": "MIT", "dependencies": { - "mimic-function": "^5.0.0" + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sindresorhus/memoize?sponsor=1" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", "dev": true, "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, "engines": { - "node": ">= 8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, "engines": { - "node": ">= 0.6" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", "dev": true, "license": "MIT", "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" }, "engines": { - "node": ">=8.6" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/micromatch/node_modules/picomatch": { + "node_modules/jest-util/node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", @@ -4332,57 +6741,30 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" }, "engines": { - "node": ">= 0.6" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "license": "MIT", "engines": { "node": ">=10" }, @@ -4390,507 +6772,3688 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", "dependencies": { - "yallist": "^4.0.0" + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" }, "engines": { - "node": ">=8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/minipass-collect": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", - "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", - "license": "ISC", - "optional": true, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", "dependencies": { - "minipass": "^3.0.0" + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" }, "engines": { - "node": ">= 8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/minipass-fetch": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", - "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "minipass": "^3.1.0", - "minipass-sized": "^1.0.3", - "minizlib": "^2.0.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" }, - "optionalDependencies": { - "encoding": "^0.1.12" + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/minipass-flush": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", - "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" }, - "node_modules/minipass-pipeline": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", - "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", - "license": "ISC", - "optional": true, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", "dependencies": { - "minipass": "^3.0.0" + "argparse": "^2.0.1" }, - "engines": { - "node": ">=8" + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/minipass-sized": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", - "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" }, "engines": { - "node": ">=8" + "node": ">=6" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/license-checker": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/license-checker/-/license-checker-25.0.1.tgz", + "integrity": "sha512-mET5AIwl7MR2IAKYYoVBBpV0OnkKQ1xGj2IMMeEFIs42QAkEVjRtFZGWmQ28WeU7MP779iAgOaOy93Mn44mn6g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "chalk": "^2.4.1", + "debug": "^3.1.0", + "mkdirp": "^0.5.1", + "nopt": "^4.0.1", + "read-installed": "~4.0.3", + "semver": "^5.5.0", + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0", + "spdx-satisfies": "^4.0.0", + "treeify": "^1.1.0" + }, + "bin": { + "license-checker": "bin/license-checker" + } + }, + "node_modules/license-checker/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/license-checker/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/license-checker/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/license-checker/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/license-checker/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/license-checker/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/license-checker/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/license-checker/node_modules/nopt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", + "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "1", + "osenv": "^0.1.4" + }, + "bin": { + "nopt": "bin/nopt.js" + } + }, + "node_modules/license-checker/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/license-checker/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/long": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.4.tgz", + "integrity": "sha512-qtzLbJE8hq7VabR3mISmVGtoXP8KGc2Z/AT8OuqlYD7JTR3oqrgwdjnk07wpj1twXxYmgDXgoKVWUG/fReSzHg==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "license": "ISC", + "optional": true, + "dependencies": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/make-fetch-happen/node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-fetch-happen/node_modules/socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/make-fetch-happen/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memoize": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/memoize/-/memoize-10.0.0.tgz", + "integrity": "sha512-H6cBLgsi6vMWOcCpvVCdFFnl3kerEXbrYh9q+lY6VXvQSmM6CkmV08VOwT+WE2tzIEqRPFfAq3fm4v/UIW6mSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/memoize?sponsor=1" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "encoding": "^0.1.12" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/minizlib": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nan": { + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", + "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", + "license": "MIT", + "optional": true + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/node-abi": { + "version": "3.74.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz", + "integrity": "sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "license": "MIT", + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 10.12.0" + } + }, + "node_modules/node-gyp/node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/node-gyp/node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^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" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/node-gyp/node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemailer": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.0.tgz", + "integrity": "sha512-SQ3wZCExjeSatLE/HBaXS5vqUOQk6GtBdIIKxiFdmm01mOQZX/POJkO3SUX1wDiYcwUOJwT23scFSC9fY2H8IA==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/nodemon": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz", + "integrity": "sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/nodemon/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/nodemon/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/nodemon/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/nodemon/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/npm/-/npm-11.1.0.tgz", + "integrity": "sha512-rPMBrZud26lI/LcjQeLw/K5Hf1apXMKgkpNNEzp0YQYmM877+T1ZNKPcB2hnTi7e6fBNz8xLtMMn/w46fVUqGw==", + "bundleDependencies": [ + "@isaacs/string-locale-compare", + "@npmcli/arborist", + "@npmcli/config", + "@npmcli/fs", + "@npmcli/map-workspaces", + "@npmcli/package-json", + "@npmcli/promise-spawn", + "@npmcli/redact", + "@npmcli/run-script", + "@sigstore/tuf", + "abbrev", + "archy", + "cacache", + "chalk", + "ci-info", + "cli-columns", + "fastest-levenshtein", + "fs-minipass", + "glob", + "graceful-fs", + "hosted-git-info", + "ini", + "init-package-json", + "is-cidr", + "json-parse-even-better-errors", + "libnpmaccess", + "libnpmdiff", + "libnpmexec", + "libnpmfund", + "libnpmorg", + "libnpmpack", + "libnpmpublish", + "libnpmsearch", + "libnpmteam", + "libnpmversion", + "make-fetch-happen", + "minimatch", + "minipass", + "minipass-pipeline", + "ms", + "node-gyp", + "nopt", + "normalize-package-data", + "npm-audit-report", + "npm-install-checks", + "npm-package-arg", + "npm-pick-manifest", + "npm-profile", + "npm-registry-fetch", + "npm-user-validate", + "p-map", + "pacote", + "parse-conflict-json", + "proc-log", + "qrcode-terminal", + "read", + "semver", + "spdx-expression-parse", + "ssri", + "supports-color", + "tar", + "text-table", + "tiny-relative-date", + "treeverse", + "validate-npm-package-name", + "which" + ], + "license": "Artistic-2.0", + "workspaces": [ + "docs", + "smoke-tests", + "mock-globals", + "mock-registry", + "workspaces/*" + ], + "dependencies": { + "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/arborist": "^9.0.0", + "@npmcli/config": "^10.0.1", + "@npmcli/fs": "^4.0.0", + "@npmcli/map-workspaces": "^4.0.2", + "@npmcli/package-json": "^6.1.1", + "@npmcli/promise-spawn": "^8.0.2", + "@npmcli/redact": "^3.0.0", + "@npmcli/run-script": "^9.0.1", + "@sigstore/tuf": "^3.0.0", + "abbrev": "^3.0.0", + "archy": "~1.0.0", + "cacache": "^19.0.1", + "chalk": "^5.4.1", + "ci-info": "^4.1.0", + "cli-columns": "^4.0.0", + "fastest-levenshtein": "^1.0.16", + "fs-minipass": "^3.0.3", + "glob": "^10.4.5", + "graceful-fs": "^4.2.11", + "hosted-git-info": "^8.0.2", + "ini": "^5.0.0", + "init-package-json": "^8.0.0", + "is-cidr": "^5.1.0", + "json-parse-even-better-errors": "^4.0.0", + "libnpmaccess": "^10.0.0", + "libnpmdiff": "^8.0.0", + "libnpmexec": "^10.0.0", + "libnpmfund": "^7.0.0", + "libnpmorg": "^8.0.0", + "libnpmpack": "^9.0.0", + "libnpmpublish": "^11.0.0", + "libnpmsearch": "^9.0.0", + "libnpmteam": "^8.0.0", + "libnpmversion": "^8.0.0", + "make-fetch-happen": "^14.0.3", + "minimatch": "^9.0.5", + "minipass": "^7.1.1", + "minipass-pipeline": "^1.2.4", + "ms": "^2.1.2", + "node-gyp": "^11.0.0", + "nopt": "^8.0.0", + "normalize-package-data": "^7.0.0", + "npm-audit-report": "^6.0.0", + "npm-install-checks": "^7.1.1", + "npm-package-arg": "^12.0.1", + "npm-pick-manifest": "^10.0.0", + "npm-profile": "^11.0.1", + "npm-registry-fetch": "^18.0.2", + "npm-user-validate": "^3.0.0", + "p-map": "^7.0.3", + "pacote": "^21.0.0", + "parse-conflict-json": "^4.0.0", + "proc-log": "^5.0.0", + "qrcode-terminal": "^0.12.0", + "read": "^4.0.0", + "semver": "^7.6.3", + "spdx-expression-parse": "^4.0.0", + "ssri": "^12.0.0", + "supports-color": "^9.4.0", + "tar": "^6.2.1", + "text-table": "~0.2.0", + "tiny-relative-date": "^1.3.0", + "treeverse": "^3.0.0", + "validate-npm-package-name": "^6.0.0", + "which": "^5.0.0" + }, + "bin": { + "npm": "bin/npm-cli.js", + "npx": "bin/npx-cli.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", + "dev": true, + "license": "ISC" + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui": { + "version": "8.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "inBundle": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/npm/node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/npm/node_modules/@isaacs/string-locale-compare": { + "version": "1.1.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/@npmcli/agent": { + "version": "3.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/arborist": { + "version": "9.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/fs": "^4.0.0", + "@npmcli/installed-package-contents": "^3.0.0", + "@npmcli/map-workspaces": "^4.0.1", + "@npmcli/metavuln-calculator": "^9.0.0", + "@npmcli/name-from-folder": "^3.0.0", + "@npmcli/node-gyp": "^4.0.0", + "@npmcli/package-json": "^6.0.1", + "@npmcli/query": "^4.0.0", + "@npmcli/redact": "^3.0.0", + "@npmcli/run-script": "^9.0.1", + "bin-links": "^5.0.0", + "cacache": "^19.0.1", + "common-ancestor-path": "^1.0.1", + "hosted-git-info": "^8.0.0", + "json-stringify-nice": "^1.1.4", + "lru-cache": "^10.2.2", + "minimatch": "^9.0.4", + "nopt": "^8.0.0", + "npm-install-checks": "^7.1.0", + "npm-package-arg": "^12.0.0", + "npm-pick-manifest": "^10.0.0", + "npm-registry-fetch": "^18.0.1", + "pacote": "^21.0.0", + "parse-conflict-json": "^4.0.0", + "proc-log": "^5.0.0", + "proggy": "^3.0.0", + "promise-all-reject-late": "^1.0.0", + "promise-call-limit": "^3.0.1", + "read-package-json-fast": "^4.0.0", + "semver": "^7.3.7", + "ssri": "^12.0.0", + "treeverse": "^3.0.0", + "walk-up-path": "^4.0.0" + }, + "bin": { + "arborist": "bin/index.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/config": { + "version": "10.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/map-workspaces": "^4.0.1", + "@npmcli/package-json": "^6.0.1", + "ci-info": "^4.0.0", + "ini": "^5.0.0", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "walk-up-path": "^4.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/fs": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/git": { + "version": "6.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/promise-spawn": "^8.0.0", + "ini": "^5.0.0", + "lru-cache": "^10.0.1", + "npm-pick-manifest": "^10.0.0", + "proc-log": "^5.0.0", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/installed-package-contents": { + "version": "3.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-bundled": "^4.0.0", + "npm-normalize-package-bin": "^4.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/map-workspaces": { + "version": "4.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/name-from-folder": "^3.0.0", + "@npmcli/package-json": "^6.0.0", + "glob": "^10.2.2", + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/metavuln-calculator": { + "version": "9.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "cacache": "^19.0.0", + "json-parse-even-better-errors": "^4.0.0", + "pacote": "^21.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/name-from-folder": { + "version": "3.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/node-gyp": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/package-json": { + "version": "6.1.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^6.0.0", + "glob": "^10.2.2", + "hosted-git-info": "^8.0.0", + "json-parse-even-better-errors": "^4.0.0", + "proc-log": "^5.0.0", + "semver": "^7.5.3", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/promise-spawn": { + "version": "8.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "which": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/query": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^6.1.2" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/redact": { + "version": "3.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/run-script": { + "version": "9.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^4.0.0", + "@npmcli/package-json": "^6.0.0", + "@npmcli/promise-spawn": "^8.0.0", + "node-gyp": "^11.0.0", + "proc-log": "^5.0.0", + "which": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "inBundle": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/npm/node_modules/@sigstore/bundle": { + "version": "3.0.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.3.2" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@sigstore/core": { + "version": "2.0.0", + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@sigstore/protobuf-specs": { + "version": "0.3.3", + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@sigstore/sign": { + "version": "3.0.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^3.0.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.3.2", + "make-fetch-happen": "^14.0.1", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@sigstore/tuf": { + "version": "3.0.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.3.2", + "tuf-js": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@sigstore/verify": { + "version": "2.0.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^3.0.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.3.2" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@tufjs/models": { + "version": "3.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^9.0.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/abbrev": { + "version": "3.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/agent-base": { + "version": "7.1.3", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/ansi-regex": { + "version": "5.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/ansi-styles": { + "version": "6.2.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/npm/node_modules/aproba": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/archy": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/balanced-match": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/bin-links": { + "version": "5.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "cmd-shim": "^7.0.0", + "npm-normalize-package-bin": "^4.0.0", + "proc-log": "^5.0.0", + "read-cmd-shim": "^5.0.0", + "write-file-atomic": "^6.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/binary-extensions": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/brace-expansion": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/npm/node_modules/cacache": { + "version": "19.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^4.0.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "tar": "^7.4.3", + "unique-filename": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/cacache/node_modules/chownr": { + "version": "3.0.0", + "inBundle": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/npm/node_modules/cacache/node_modules/minizlib": { + "version": "3.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.4", + "rimraf": "^5.0.5" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/npm/node_modules/cacache/node_modules/mkdirp": { + "version": "3.0.1", + "inBundle": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/cacache/node_modules/tar": { + "version": "7.4.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/npm/node_modules/cacache/node_modules/yallist": { + "version": "5.0.0", + "inBundle": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/npm/node_modules/chalk": { + "version": "5.4.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/npm/node_modules/chownr": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/ci-info": { + "version": "4.1.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/cidr-regex": { + "version": "4.1.1", + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "ip-regex": "^5.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/npm/node_modules/cli-columns": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/npm/node_modules/cmd-shim": { + "version": "7.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/color-convert": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/npm/node_modules/color-name": { + "version": "1.1.4", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/common-ancestor-path": { + "version": "1.0.1", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/cross-spawn": { + "version": "7.0.6", + "inBundle": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/cssesc": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/debug": { + "version": "4.4.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/npm/node_modules/diff": { + "version": "7.0.0", + "inBundle": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/npm/node_modules/eastasianwidth": { + "version": "0.2.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/emoji-regex": { + "version": "8.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/encoding": { + "version": "0.1.13", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/npm/node_modules/env-paths": { + "version": "2.2.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/err-code": { + "version": "2.0.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/exponential-backoff": { + "version": "3.1.1", + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/npm/node_modules/fastest-levenshtein": { + "version": "1.0.16", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/npm/node_modules/foreground-child": { + "version": "3.3.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/fs-minipass": { + "version": "3.0.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/glob": { + "version": "10.4.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/graceful-fs": { + "version": "4.2.11", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/hosted-git-info": { + "version": "8.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/http-cache-semantics": { + "version": "4.1.1", + "inBundle": true, + "license": "BSD-2-Clause" + }, + "node_modules/npm/node_modules/http-proxy-agent": { + "version": "7.0.2", + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/https-proxy-agent": { + "version": "7.0.6", + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/iconv-lite": { + "version": "0.6.3", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/ignore-walk": { + "version": "7.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/imurmurhash": { + "version": "0.1.4", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/npm/node_modules/ini": { + "version": "5.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/init-package-json": { + "version": "8.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/package-json": "^6.1.0", + "npm-package-arg": "^12.0.0", + "promzard": "^2.0.0", + "read": "^4.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4", + "validate-npm-package-name": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/ip-address": { + "version": "9.0.5", + "inBundle": true, + "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/npm/node_modules/ip-regex": { + "version": "5.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/is-cidr": { + "version": "5.1.0", + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "cidr-regex": "^4.1.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/npm/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/isexe": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/jackspeak": { + "version": "3.4.3", + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/npm/node_modules/jsbn": { + "version": "1.1.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/json-parse-even-better-errors": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/json-stringify-nice": { + "version": "1.1.4", + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/jsonparse": { + "version": "1.3.1", + "engines": [ + "node >= 0.2.0" + ], + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/just-diff": { + "version": "6.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/just-diff-apply": { + "version": "5.5.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/libnpmaccess": { + "version": "10.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-package-arg": "^12.0.0", + "npm-registry-fetch": "^18.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmdiff": { + "version": "8.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^9.0.0", + "@npmcli/installed-package-contents": "^3.0.0", + "binary-extensions": "^3.0.0", + "diff": "^7.0.0", + "minimatch": "^9.0.4", + "npm-package-arg": "^12.0.0", + "pacote": "^21.0.0", + "tar": "^6.2.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmexec": { + "version": "10.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^9.0.0", + "@npmcli/run-script": "^9.0.1", + "ci-info": "^4.0.0", + "npm-package-arg": "^12.0.0", + "pacote": "^21.0.0", + "proc-log": "^5.0.0", + "read": "^4.0.0", + "read-package-json-fast": "^4.0.0", + "semver": "^7.3.7", + "walk-up-path": "^4.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmfund": { + "version": "7.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^9.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmorg": { + "version": "8.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^18.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmpack": { + "version": "9.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^9.0.0", + "@npmcli/run-script": "^9.0.1", + "npm-package-arg": "^12.0.0", + "pacote": "^21.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmpublish": { + "version": "11.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "ci-info": "^4.0.0", + "normalize-package-data": "^7.0.0", + "npm-package-arg": "^12.0.0", + "npm-registry-fetch": "^18.0.1", + "proc-log": "^5.0.0", + "semver": "^7.3.7", + "sigstore": "^3.0.0", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmsearch": { + "version": "9.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-registry-fetch": "^18.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmteam": { + "version": "8.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^18.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmversion": { + "version": "8.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^6.0.1", + "@npmcli/run-script": "^9.0.1", + "json-parse-even-better-errors": "^4.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.7" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/lru-cache": { + "version": "10.4.3", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/make-fetch-happen": { + "version": "14.0.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^3.0.0", + "cacache": "^19.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/make-fetch-happen/node_modules/negotiator": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/npm/node_modules/minimatch": { + "version": "9.0.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/minipass": { + "version": "7.1.2", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/npm/node_modules/minipass-collect": { + "version": "2.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/npm/node_modules/minipass-fetch": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/npm/node_modules/minipass-fetch/node_modules/minizlib": { + "version": "3.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.4", + "rimraf": "^5.0.5" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/npm/node_modules/minipass-flush": { + "version": "1.0.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-pipeline": { + "version": "1.2.4", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-sized": { + "version": "1.0.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minizlib": { + "version": "2.1.2", + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/mkdirp": { + "version": "1.0.4", + "inBundle": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/ms": { + "version": "2.1.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/mute-stream": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/node-gyp": { + "version": "11.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^14.0.3", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "tar": "^7.4.3", + "which": "^5.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/chownr": { + "version": "3.0.0", + "inBundle": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/minizlib": { + "version": "3.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.4", + "rimraf": "^5.0.5" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/mkdirp": { + "version": "3.0.1", + "inBundle": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/tar": { + "version": "7.4.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/yallist": { + "version": "5.0.0", + "inBundle": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/npm/node_modules/nopt": { + "version": "8.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/nopt/node_modules/abbrev": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/normalize-package-data": { + "version": "7.0.0", + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^8.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-audit-report": { + "version": "6.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-bundled": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-install-checks": { + "version": "7.1.1", + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-normalize-package-bin": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-package-arg": { + "version": "12.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^6.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-packlist": { + "version": "10.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "ignore-walk": "^7.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/npm-pick-manifest": { + "version": "10.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^7.1.0", + "npm-normalize-package-bin": "^4.0.0", + "npm-package-arg": "^12.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-profile": { + "version": "11.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-registry-fetch": "^18.0.0", + "proc-log": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-registry-fetch": { + "version": "18.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/redact": "^3.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^14.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minizlib": "^3.0.1", + "npm-package-arg": "^12.0.0", + "proc-log": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-registry-fetch/node_modules/minizlib": { + "version": "3.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.4", + "rimraf": "^5.0.5" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/npm/node_modules/npm-user-validate": { + "version": "3.0.0", + "inBundle": true, + "license": "BSD-2-Clause", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/p-map": { + "version": "7.0.3", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/package-json-from-dist": { + "version": "1.0.1", + "inBundle": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/npm/node_modules/pacote": { + "version": "21.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^6.0.0", + "@npmcli/installed-package-contents": "^3.0.0", + "@npmcli/package-json": "^6.0.0", + "@npmcli/promise-spawn": "^8.0.0", + "@npmcli/run-script": "^9.0.0", + "cacache": "^19.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^12.0.0", + "npm-packlist": "^10.0.0", + "npm-pick-manifest": "^10.0.0", + "npm-registry-fetch": "^18.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "sigstore": "^3.0.0", + "ssri": "^12.0.0", + "tar": "^6.1.11" + }, + "bin": { + "pacote": "bin/index.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/parse-conflict-json": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^4.0.0", + "just-diff": "^6.0.0", + "just-diff-apply": "^5.2.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/path-key": { + "version": "3.1.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/path-scurry": { + "version": "1.11.1", + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "inBundle": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/proc-log": { + "version": "5.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/proggy": { + "version": "3.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/promise-all-reject-late": { + "version": "1.0.1", + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/promise-call-limit": { + "version": "3.0.2", + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/promise-inflight": { + "version": "1.0.1", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/promise-retry": { + "version": "2.0.1", + "inBundle": true, "license": "MIT", "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" + "err-code": "^2.0.2", + "retry": "^0.12.0" }, "engines": { - "node": ">= 8" + "node": ">=10" } }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "license": "MIT", + "node_modules/npm/node_modules/promzard": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC", "dependencies": { - "minimist": "^1.2.6" + "read": "^4.0.0" }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/qrcode-terminal": { + "version": "0.12.0", + "inBundle": true, "bin": { - "mkdirp": "bin/cmd.js" + "qrcode-terminal": "bin/qrcode-terminal.js" } }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "license": "MIT" + "node_modules/npm/node_modules/read": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "mute-stream": "^2.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } }, - "node_modules/moment": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", - "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "node_modules/npm/node_modules/read-cmd-shim": { + "version": "5.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/read-package-json-fast": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^4.0.0", + "npm-normalize-package-bin": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/retry": { + "version": "0.12.0", + "inBundle": true, "license": "MIT", "engines": { - "node": "*" + "node": ">= 4" } }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" + "node_modules/npm/node_modules/rimraf": { + "version": "5.0.10", + "inBundle": true, + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, - "node_modules/nan": { - "version": "2.22.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", - "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", + "node_modules/npm/node_modules/safer-buffer": { + "version": "2.1.2", + "inBundle": true, "license": "MIT", "optional": true }, - "node_modules/napi-build-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", - "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", - "license": "MIT" + "node_modules/npm/node_modules/semver": { + "version": "7.6.3", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" + "node_modules/npm/node_modules/shebang-command": { + "version": "2.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "node_modules/npm/node_modules/shebang-regex": { + "version": "3.0.0", + "inBundle": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=8" } }, - "node_modules/node-abi": { - "version": "3.71.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.71.0.tgz", - "integrity": "sha512-SZ40vRiy/+wRTf21hxkkEjPJZpARzUMVcJoQse2EF8qkUWbbO2z7vd5oA/H6bVH6SZQ5STGcu0KRDS7biNRfxw==", + "node_modules/npm/node_modules/signal-exit": { + "version": "4.1.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/sigstore": { + "version": "3.0.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^3.0.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.3.2", + "@sigstore/sign": "^3.0.0", + "@sigstore/tuf": "^3.0.0", + "@sigstore/verify": "^2.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/smart-buffer": { + "version": "4.2.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/npm/node_modules/socks": { + "version": "2.8.3", + "inBundle": true, "license": "MIT", "dependencies": { - "semver": "^7.3.5" + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" }, "engines": { - "node": ">=10" + "node": ">= 10.0.0", + "npm": ">= 3.0.0" } }, - "node_modules/node-addon-api": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", - "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", - "license": "MIT" + "node_modules/npm/node_modules/socks-proxy-agent": { + "version": "8.0.5", + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "node_modules/npm/node_modules/spdx-correct": { + "version": "3.2.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-correct/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "inBundle": true, "license": "MIT", "dependencies": { - "whatwg-url": "^5.0.0" + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-exceptions": { + "version": "2.5.0", + "inBundle": true, + "license": "CC-BY-3.0" + }, + "node_modules/npm/node_modules/spdx-expression-parse": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-license-ids": { + "version": "3.0.21", + "inBundle": true, + "license": "CC0-1.0" + }, + "node_modules/npm/node_modules/sprintf-js": { + "version": "1.1.3", + "inBundle": true, + "license": "BSD-3-Clause" + }, + "node_modules/npm/node_modules/ssri": { + "version": "12.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" }, "engines": { - "node": "4.x || >=6.0.0" + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/string-width": { + "version": "4.2.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, - "peerDependencies": { - "encoding": "^0.1.0" + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } + "engines": { + "node": ">=8" } }, - "node_modules/node-gyp": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", - "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "node_modules/npm/node_modules/strip-ansi": { + "version": "6.0.1", + "inBundle": true, "license": "MIT", - "optional": true, "dependencies": { - "env-paths": "^2.2.0", - "glob": "^7.1.4", - "graceful-fs": "^4.2.6", - "make-fetch-happen": "^9.1.0", - "nopt": "^5.0.0", - "npmlog": "^6.0.0", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.2", - "which": "^2.0.2" + "ansi-regex": "^5.0.1" }, - "bin": { - "node-gyp": "bin/node-gyp.js" + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">= 10.12.0" + "node": ">=8" + } + }, + "node_modules/npm/node_modules/supports-color": { + "version": "9.4.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/node-gyp/node_modules/are-we-there-yet": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", - "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", - "deprecated": "This package is no longer supported.", + "node_modules/npm/node_modules/tar": { + "version": "6.2.1", + "inBundle": true, "license": "ISC", - "optional": true, "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": ">=10" } }, - "node_modules/node-gyp/node_modules/gauge": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", - "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", - "deprecated": "This package is no longer supported.", + "node_modules/npm/node_modules/tar/node_modules/fs-minipass": { + "version": "2.1.0", + "inBundle": true, "license": "ISC", - "optional": true, "dependencies": { - "aproba": "^1.0.3 || ^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" + "minipass": "^3.0.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": ">= 8" } }, - "node_modules/node-gyp/node_modules/npmlog": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", - "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", - "deprecated": "This package is no longer supported.", + "node_modules/npm/node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "inBundle": true, "license": "ISC", - "optional": true, "dependencies": { - "are-we-there-yet": "^3.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^4.0.3", - "set-blocking": "^2.0.0" + "yallist": "^4.0.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": ">=8" } }, - "node_modules/nodemailer": { - "version": "6.9.16", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.16.tgz", - "integrity": "sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==", - "license": "MIT-0", + "node_modules/npm/node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "inBundle": true, + "license": "ISC", "engines": { - "node": ">=6.0.0" + "node": ">=8" } }, - "node_modules/nodemon": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz", - "integrity": "sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chokidar": "^3.5.2", - "debug": "^4", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.1.2", - "pstree.remy": "^1.1.8", - "semver": "^7.5.3", - "simple-update-notifier": "^2.0.0", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.5" - }, - "bin": { - "nodemon": "bin/nodemon.js" - }, + "node_modules/npm/node_modules/text-table": { + "version": "0.2.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/tiny-relative-date": { + "version": "1.3.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/treeverse": { + "version": "3.0.0", + "inBundle": true, + "license": "ISC", "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nodemon" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/nodemon/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, + "node_modules/npm/node_modules/tuf-js": { + "version": "3.0.1", + "inBundle": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "@tufjs/models": "3.0.1", + "debug": "^4.3.6", + "make-fetch-happen": "^14.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/nodemon/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", + "node_modules/npm/node_modules/unique-filename": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "unique-slug": "^5.0.0" }, "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/nodemon/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, + "node_modules/npm/node_modules/unique-slug": { + "version": "5.0.0", + "inBundle": true, "license": "ISC", "dependencies": { - "is-glob": "^4.0.1" + "imurmurhash": "^0.1.4" }, "engines": { - "node": ">= 6" + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/nodemon/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, + "node_modules/npm/node_modules/util-deprecate": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/validate-npm-package-license": { + "version": "3.0.4", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/npm/node_modules/validate-npm-package-license/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "inBundle": true, "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/validate-npm-package-name": { + "version": "6.0.0", + "inBundle": true, + "license": "ISC", "engines": { - "node": ">=4" + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/nodemon/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, + "node_modules/npm/node_modules/walk-up-path": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/npm/node_modules/which": { + "version": "5.0.0", + "inBundle": true, "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" }, "engines": { - "node": "*" + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/nodemon/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, + "node_modules/npm/node_modules/which/node_modules/isexe": { + "version": "3.1.1", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/npm/node_modules/wrap-ansi": { + "version": "8.1.0", + "inBundle": true, "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/nodemon/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, + "node_modules/npm/node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "inBundle": true, "license": "MIT", "dependencies": { - "picomatch": "^2.2.1" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=8.10.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/nodemon/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, + "node_modules/npm/node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "inBundle": true, "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "color-convert": "^2.0.1" }, "engines": { - "node": ">=4" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "license": "ISC", - "dependencies": { - "abbrev": "1" + "node_modules/npm/node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.1.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" }, - "bin": { - "nopt": "bin/nopt.js" + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "9.2.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/string-width": { + "version": "5.1.2", + "inBundle": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" }, "engines": { - "node": ">=6" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "license": "BSD-2-Clause", + "node_modules/npm/node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "inBundle": true, + "license": "MIT", "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/normalize-package-data/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, + "node_modules/npm/node_modules/write-file-atomic": { + "version": "6.0.0", + "inBundle": true, "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, "engines": { - "node": ">=0.10.0" + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/npm-normalize-package-bin": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", - "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", - "dev": true, + "node_modules/npm/node_modules/yallist": { + "version": "4.0.0", + "inBundle": true, "license": "ISC" }, "node_modules/npmlog": { @@ -4925,9 +10488,9 @@ } }, "node_modules/object-inspect": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", - "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -4967,28 +10530,21 @@ } }, "node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, "license": "MIT", "dependencies": { - "mimic-function": "^5.0.0" + "mimic-fn": "^2.1.0" }, "engines": { - "node": ">=18" + "node": ">=6" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/openapi-types": { - "version": "12.1.3", - "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", - "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", - "license": "MIT", - "peer": true - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5007,97 +10563,6 @@ "node": ">= 0.8.0" } }, - "node_modules/ora": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-8.1.1.tgz", - "integrity": "sha512-YWielGi1XzG1UTvOaCFaNgEnuhZVMSHYkW/FQ7UX8O26PtlpdM84c0f7wLPlkvx2RfiQmnzd61d/MGxmpQeJPw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^5.3.0", - "cli-cursor": "^5.0.0", - "cli-spinners": "^2.9.2", - "is-interactive": "^2.0.0", - "is-unicode-supported": "^2.0.0", - "log-symbols": "^6.0.0", - "stdin-discarder": "^0.2.2", - "string-width": "^7.2.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ora/node_modules/chalk": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/ora/node_modules/emoji-regex": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", - "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", - "dev": true, - "license": "MIT" - }, - "node_modules/ora/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/os-homedir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", @@ -5178,11 +10643,74 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.1.0.tgz", + "integrity": "sha512-Z5FnLVVZSnX7WjBg0mhDtydeRZ1xMcATZThjySQUHqr+0ksP8kqaw23fNKkaaN/Z8gwLUs/W7xdl0I75eP2Xyw==", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -5191,6 +10719,24 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -5242,11 +10788,16 @@ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -5262,42 +10813,89 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/playwright": { - "version": "1.49.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.1.tgz", - "integrity": "sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==", + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", "dependencies": { - "playwright-core": "1.49.1" + "find-up": "^4.0.0" }, - "bin": { - "playwright": "cli.js" + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" }, "engines": { - "node": ">=18" + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" }, - "optionalDependencies": { - "fsevents": "2.3.2" + "engines": { + "node": ">=8" } }, - "node_modules/playwright-core": { - "version": "1.49.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.1.tgz", - "integrity": "sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==", + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" }, "engines": { - "node": ">=18" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" } }, "node_modules/prebuild-install": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", - "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", "license": "MIT", "dependencies": { "detect-libc": "^2.0.0", @@ -5305,7 +10903,7 @@ "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^1.0.1", + "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", @@ -5346,6 +10944,43 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -5381,28 +11016,108 @@ "node": ">= 6" } }, + "node_modules/protobufjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "license": "MIT", "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" }, "engines": { - "node": ">= 0.10" + "node": ">= 14" } }, - "node_modules/proxy-addr/node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", "engines": { - "node": ">= 0.10" + "node": ">=12" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", @@ -5430,6 +11145,61 @@ "node": ">=6" } }, + "node_modules/puppeteer": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.2.0.tgz", + "integrity": "sha512-z8vv7zPEgrilIbOo3WNvM+2mXMnyM9f4z6zdrB88Fzeuo43Oupmjrzk3EpuvuCtyK0A7Lsllfx7Z+4BvEEGJcQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.7.1", + "chromium-bidi": "1.2.0", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1402036", + "puppeteer-core": "24.2.0", + "typed-query-selector": "^2.12.0" + }, + "bin": { + "puppeteer": "lib/cjs/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.2.0.tgz", + "integrity": "sha512-e4A4/xqWdd4kcE6QVHYhJ+Qlx/+XpgjP4d8OwBx0DJoY/nkIRhSgYmKQnv7+XSs1ofBstalt+XPGrkaz4FoXOQ==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.7.1", + "chromium-bidi": "1.2.0", + "debug": "^4.4.0", + "devtools-protocol": "0.0.1402036", + "typed-query-selector": "^2.12.0", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, "node_modules/qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", @@ -5520,6 +11290,13 @@ "node": ">=0.10.0" } }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/read-installed": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/read-installed/-/read-installed-4.0.3.tgz", @@ -5592,12 +11369,12 @@ } }, "node_modules/readdirp": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", - "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.1.tgz", + "integrity": "sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==", "license": "MIT", "engines": { - "node": ">= 14.16.0" + "node": ">= 14.18.0" }, "funding": { "type": "individual", @@ -5627,6 +11404,15 @@ "regexp-tree": "bin/regexp-tree" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -5658,11 +11444,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -5678,34 +11486,14 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", "dev": true, "license": "MIT", - "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/restore-cursor/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=10" } }, "node_modules/retry": { @@ -5815,9 +11603,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -6056,6 +11844,12 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -6076,6 +11870,16 @@ "dev": true, "license": "MIT" }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/slide": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", @@ -6091,7 +11895,6 @@ "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", "license": "MIT", - "optional": true, "engines": { "node": ">= 6.0.0", "npm": ">= 3.0.0" @@ -6102,7 +11905,6 @@ "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", "license": "MIT", - "optional": true, "dependencies": { "ip-address": "^9.0.5", "smart-buffer": "^4.2.0" @@ -6113,18 +11915,47 @@ } }, "node_modules/socks-proxy-agent": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", - "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", "license": "MIT", - "optional": true, "dependencies": { - "agent-base": "^6.0.2", - "debug": "^4.3.3", - "socks": "^2.6.2" + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" }, "engines": { - "node": ">= 10" + "node": ">= 14" + } + }, + "node_modules/socks-proxy-agent/node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" } }, "node_modules/spdx-compare": { @@ -6169,9 +12000,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.20", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz", - "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==", + "version": "3.0.21", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", + "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", "dev": true, "license": "CC0-1.0" }, @@ -6204,8 +12035,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "license": "BSD-3-Clause", - "optional": true + "license": "BSD-3-Clause" }, "node_modules/sqlite3": { "version": "5.1.7", @@ -6276,6 +12106,29 @@ "node": "*" } }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -6285,17 +12138,17 @@ "node": ">= 0.8" } }, - "node_modules/stdin-discarder": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", - "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", - "dev": true, + "node_modules/streamx": { + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.0.tgz", + "integrity": "sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==", "license": "MIT", - "engines": { - "node": ">=18" + "dependencies": { + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "optionalDependencies": { + "bare-events": "^2.2.0" } }, "node_modules/string_decoder": { @@ -6307,6 +12160,20 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -6334,13 +12201,23 @@ } }, "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", "dev": true, "license": "MIT", "engines": { - "node": ">=4" + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" } }, "node_modules/strip-json-comments": { @@ -6356,120 +12233,84 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/superagent": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-9.0.2.tgz", + "integrity": "sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w==", "dev": true, "license": "MIT", "dependencies": { - "has-flag": "^4.0.0" + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.4", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^3.5.1", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.0" }, "engines": { - "node": ">=8" + "node": ">=14.18.0" } }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/swagger-jsdoc": { - "version": "6.2.8", - "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz", - "integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==", - "license": "MIT", - "dependencies": { - "commander": "6.2.0", - "doctrine": "3.0.0", - "glob": "7.1.6", - "lodash.mergewith": "^4.6.2", - "swagger-parser": "^10.0.3", - "yaml": "2.0.0-1" - }, "bin": { - "swagger-jsdoc": "bin/swagger-jsdoc.js" + "mime": "cli.js" }, "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/swagger-jsdoc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "node": ">=4.0.0" } }, - "node_modules/swagger-jsdoc/node_modules/commander": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", - "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", + "node_modules/supertest": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.0.0.tgz", + "integrity": "sha512-qlsr7fIC0lSddmA3tzojvzubYxvlGtzumcdHgPwbFWMISQwL22MhM2Y3LNt+6w9Yyx7559VW5ab70dgphm8qQA==", + "dev": true, "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/swagger-jsdoc/node_modules/glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "license": "ISC", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "methods": "^1.1.2", + "superagent": "^9.0.1" }, "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=14.18.0" } }, - "node_modules/swagger-jsdoc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "license": "ISC", + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" + "has-flag": "^4.0.0" }, "engines": { - "node": "*" + "node": ">=8" } }, - "node_modules/swagger-parser": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz", - "integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==", + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, "license": "MIT", - "dependencies": { - "@apidevtools/swagger-parser": "10.0.3" - }, "engines": { - "node": ">=10" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/swagger-ui-dist": { - "version": "5.18.2", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.18.2.tgz", - "integrity": "sha512-J+y4mCw/zXh1FOj5wGJvnAajq6XgHOyywsa9yITmwxIlJbMqITq3gYRZHaeqLVH/eV/HOPphE6NjF+nbSNC5Zw==", + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.18.3.tgz", + "integrity": "sha512-G33HFW0iFNStfY2x6QXO2JYVMrFruc8AZRX0U/L71aA7WeWfX2E5Nm8E/tsipSZJeIZZbSjUDeynLK/wcuNWIw==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "=1.4.0" @@ -6572,6 +12413,12 @@ "node": ">=10" } }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/teamcity-service-messages": { "version": "0.1.14", "resolved": "https://registry.npmjs.org/teamcity-service-messages/-/teamcity-service-messages-0.1.14.tgz", @@ -6579,12 +12426,67 @@ "dev": true, "license": "MIT" }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, "node_modules/text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", "license": "MIT" }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6643,16 +12545,65 @@ } }, "node_modules/ts-api-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", - "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", + "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==", "dev": true, "license": "MIT", "engines": { - "node": ">=16" + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-jest": { + "version": "29.2.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", + "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.6.3", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" }, "peerDependencies": { - "typescript": ">=4.2.0" + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } } }, "node_modules/ts-node": { @@ -6730,6 +12681,22 @@ "node": ">=10.13.0" } }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/tsx": { "version": "4.19.2", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz", @@ -6750,21 +12717,6 @@ "fsevents": "~2.3.3" } }, - "node_modules/tsx/node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -6796,6 +12748,29 @@ "node": ">= 0.8.0" } }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -6809,11 +12784,17 @@ "node": ">= 0.6" } }, + "node_modules/typed-query-selector": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", + "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", + "license": "MIT" + }, "node_modules/typescript": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", - "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", - "dev": true, + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "devOptional": true, "license": "Apache-2.0", "peer": true, "bin": { @@ -6825,15 +12806,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.19.0.tgz", - "integrity": "sha512-Ni8sUkVWYK4KAcTtPjQ/UTiRk6jcsuDhPpxULapUDi8A/l8TSBk+t1GtJA1RsCzIJg0q6+J7bf35AwQigENWRQ==", + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.23.0.tgz", + "integrity": "sha512-/LBRo3HrXr5LxmrdYSOCvoAMm7p2jNizNfbIpCgvG4HMsnoprRUOce/+8VJ9BDYWW68rqIENE/haVLWPeFZBVQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.19.0", - "@typescript-eslint/parser": "8.19.0", - "@typescript-eslint/utils": "8.19.0" + "@typescript-eslint/eslint-plugin": "8.23.0", + "@typescript-eslint/parser": "8.23.0", + "@typescript-eslint/utils": "8.23.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6871,7 +12852,6 @@ "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, "license": "MIT" }, "node_modules/unique-filename": { @@ -6903,6 +12883,37 @@ "node": ">= 0.8" } }, + "node_modules/update-browserslist-db": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", + "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -6935,6 +12946,19 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -6942,6 +12966,21 @@ "dev": true, "license": "MIT" }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -6953,15 +12992,6 @@ "spdx-expression-parse": "^3.0.0" } }, - "node_modules/validator": { - "version": "13.12.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", - "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -6971,6 +13001,16 @@ "node": ">= 0.8" } }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, "node_modules/watskeburt": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/watskeburt/-/watskeburt-4.2.2.tgz", @@ -7089,25 +13129,156 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", + "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 + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, "license": "ISC" }, "node_modules/yaml": { - "version": "2.0.0-1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", - "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, "engines": { - "node": ">= 6" + "node": ">= 14" + } + }, + "node_modules/yamljs": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz", + "integrity": "sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "glob": "^7.0.5" + }, + "bin": { + "json2yaml": "bin/json2yaml", + "yaml2json": "bin/yaml2json" + } + }, + "node_modules/yamljs/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/yamljs/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" } }, "node_modules/yn": { @@ -7133,34 +13304,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/z-schema": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", - "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", - "license": "MIT", - "dependencies": { - "lodash.get": "^4.4.2", - "lodash.isequal": "^4.5.0", - "validator": "^13.7.0" - }, - "bin": { - "z-schema": "bin/z-schema" - }, - "engines": { - "node": ">=8.0.0" - }, - "optionalDependencies": { - "commander": "^9.4.1" - } - }, - "node_modules/z-schema/node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "node_modules/zod": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", "license": "MIT", - "optional": true, - "engines": { - "node": "^12.20.0 || >=14" + "funding": { + "url": "https://github.com/sponsors/colinhacks" } } } diff --git a/package.json b/package.json index 6b38fd6..c48ee73 100644 --- a/package.json +++ b/package.json @@ -1,24 +1,31 @@ { "name": "dockstatapi", + "repository": "git@github.com:Its4Nik/dockstatapi.git", "version": "2.0.1", "description": "API for docker hosts using dockerode", "main": "src/server.ts", "scripts": { + "test": "NODE_ENV=testing jest -w 1 --forceExit", + "test:silent": "NODE_ENV=testing jest -w 1 --forceExit --silent", "local-env-file": "bash ./src/misc/createEnvDev.sh", - "start": "npm run local-env-file && tsx src/server.ts", - "dev": "npm run local-env-file && nodemon", - "dev:trace": "npm run local-env-file && nodemon --trace-uncaught --trace-warnings", + "start": "npm run local-env-file && NODE_ENV=production tsx src/server.ts", + "start:build": "npm run local-env-file -d && npm run build && NODE_ENV=production node dist/src/src/server.js", + "dev": "npm run local-env-file && NODE_ENV=development nodemon", + "dev:socket": "docker compose -f docker/docker-compose.dev.yaml up -d && npm run local-env-file && NODE_ENV=development nodemon ; docker compose -f docker/docker-compose.dev.yaml down", + "dev:trace": "npm run local-env-file && NODE_ENV=development nodemon --trace-uncaught --trace-warnings", "dep": "bash ./src/misc/dependencyGraphs/createDependencyGraph.sh", "dep:remove": "bash ./src/misc/removeUnusedDeps.sh && npm run dep", - "build": "npx tsc", - "build:mini": "npx tsc && bash ./src/misc/minifyDist.sh --build-only", + "build": "tsc", + "build:mini": "tsc && bash ./src/misc/minifyDist.sh --build-only", "build:docker": "docker build . -t \"dockstatapi:local\" -f ./docker/Dockerfile-dev", + "build:docker:prod": "docker build . -t \"dockstatapi:local\" -f ./docker/Dockerfile-base", "mini": "bash ./src/misc/minifyDist.sh", "docker": "docker compose -f docker/docker-compose.yaml up -d && bash ./src/misc/.tmux.sh; docker compose -f docker/docker-compose.yaml down", "docker:build": "npm run build:docker && npm run docker", - "prettier": "npx prettier -c ./src/**/*.ts --parser typescript --write && npx prettier -c ./.github/workflows/*.yaml --parser yaml --write && npx prettier -c ./**/*.md --parser markdown --write && npx prettier -c ./**/*.json --parser json --write", - "lint": "npx eslint", - "lint:fix": "npx eslint --fix", + "docker:build:prod": "npm run build:docker:prod && npm run docker", + "prettier": "prettier -c ./__tests__/*.spec.ts --parser typescript --write && prettier -c ./src/**/*.ts --parser typescript --write && prettier -c ./.github/workflows/*.yaml --parser yaml --write && prettier -c ./**/*.md --parser markdown --write && prettier -c ./**/*.json --parser json --write", + "lint": "eslint", + "lint:fix": "eslint --fix", "license": "bash ./src/misc/credits.sh", "finish": "npm run local-env-file && npm run license && npm run prettier && npm run lint" }, @@ -29,40 +36,52 @@ "bcrypt": "^5.1.1", "chokidar": "^4.0.1", "cors": "^2.8.5", + "cytoscape": "^3.30.4", + "docker-compose": "^1.1.0", "dockerode": "^4.0.2", "express": "^4.21.1", "express-rate-limit": "^7.4.1", "https": "^1.0.0", + "i": "^0.3.7", "ipaddr.js": "^2.2.0", "nodemailer": "^6.9.16", + "npm": "^11.0.0", + "puppeteer": "^24.0.0", "sqlite3": "^5.1.7", - "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1", "winston": "^3.15.0", - "winston-daily-rotate-file": "^5.0.0" + "winston-daily-rotate-file": "^5.0.0", + "yamljs": "^0.3.0" }, "devDependencies": { "@eslint/js": "^9.17.0", - "@playwright/test": "^1.49.0", "@types/bcrypt": "^5.0.2", "@types/cors": "^2.8.17", + "@types/cytoscape": "^3.21.8", "@types/dockerode": "^3.3.31", "@types/express": "^5.0.0", "@types/express-handlebars": "^5.3.1", + "@types/jest": "^29.5.14", "@types/node": "^22.9.0", + "@types/node-fetch": "^2.6.12", "@types/nodemailer": "^6.4.17", + "@types/supertest": "^6.0.2", "@types/supports-color": "^8.1.3", "@types/swagger-jsdoc": "^6.0.4", "@types/swagger-ui-express": "^4.1.7", + "@types/ws": "^8.5.14", + "@types/yamljs": "^0.2.34", "@typescript-eslint/eslint-plugin": "^8.18.2", "@typescript-eslint/parser": "^8.18.2", "dependency-cruiser": "^16.5.0", "eslint": "^9.17.0", "globals": "^15.14.0", + "jest": "^29.7.0", "license-checker": "^25.0.1", "nodemon": "^3.1.7", - "ora": "^8.1.1", "prettier": "^3.4.2", + "supertest": "^7.0.0", + "ts-jest": "^29.2.5", "ts-node": "^10.9.2", "tsx": "^4.19.2", "typescript-eslint": "^8.18.2", @@ -71,5 +90,34 @@ "engines": { "npm": ">=10.8.2" }, - "repository": "git@github.com:Its4Nik/dockstatapi.git" + "jest": { + "preset": "ts-jest", + "testMatch": [ + "**/__tests__/**/*.(test|spec).ts" + ], + "testEnvironment": "node", + "transform": { + "^.+\\.(ts|tsx)$": "ts-jest" + }, + "moduleFileExtensions": [ + "ts", + "tsx", + "js", + "jsx", + "json", + "node" + ], + "coveragePathIgnorePatterns": [ + "/node_modules/" + ], + "moduleNameMapper": { + "^@/(.*)$": "src/$1" + }, + "transformIgnorePatterns": [ + "/node_modules/" + ], + "testPathIgnorePatterns": [ + "util" + ] + } } diff --git a/playwright.config.ts b/playwright.config.ts deleted file mode 100644 index 2c33a93..0000000 --- a/playwright.config.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { defineConfig, devices } from '@playwright/test'; - -export default defineConfig({ - timeout: 300000, - testDir: './tests', - fullyParallel: true, - forbidOnly: !!process.env.CI, - retries: process.env.CI ? 2 : 0, - workers: process.env.CI ? 1 : undefined, - reporter: 'html', - use: { - trace: 'on-first-retry', - }, - - projects: [ - { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, - }, - - { - name: 'firefox', - use: { ...devices['Desktop Firefox'] }, - }, - - { - name: 'webkit', - use: { ...devices['Desktop Safari'] }, - }, - ], - - webServer: { - command: 'npm run start', - url: 'http://127.0.0.1:9876', - reuseExistingServer: true - }, -}); diff --git a/src/config/db.ts b/src/config/db.ts index edfe383..5ed4d6a 100644 --- a/src/config/db.ts +++ b/src/config/db.ts @@ -14,7 +14,7 @@ const db: sqlite3.Database = new sqlite3.Database(dbPath, (error: unknown) => { timestamp DATETIME DEFAULT CURRENT_TIMESTAMP )`, () => { - logger.info("Database created / opened successfully, table is ready."); + logger.info("Database created / checked successfully, table is ready."); }, ); } diff --git a/src/config/hostsystem.ts b/src/config/hostsystem.ts index 0af379f..87928a8 100644 --- a/src/config/hostsystem.ts +++ b/src/config/hostsystem.ts @@ -3,7 +3,8 @@ import { VERSION, HA_MASTER, HA_UNSAFE, - TRUSTED_PROXYS, + TRUSTED_PROXIES, + LOG_LEVEL, } from "./variables"; import fs from "fs"; import logger from "../utils/logger"; @@ -16,7 +17,15 @@ const version: string = VERSION || "unknown"; const masterNode: string = HA_MASTER === "true" ? "✓" : "✗"; const unsafeSync: string = HA_UNSAFE === "true" ? "✓" : "✗"; -function writeUserConf() { +let trustedProxies: string = ""; + +if (TRUSTED_PROXIES) { + trustedProxies = TRUSTED_PROXIES; +} else { + trustedProxies = "✗"; +} + +function writeUserConf(port: number) { let previousConfig = null; let shouldRewriteConfig = false; @@ -64,6 +73,7 @@ function writeUserConf() { logger.info("-----------------------------------------"); logger.info(`Starting at : ${startDetails.startedAt}`); + logger.info(`Running env : ${process.env.NODE_ENV}`); logger.info(`Version : ${startDetails.backendVersion}`); logger.info(`Docker : ${installationDetails.inDocker}`); logger.info(`Running as : ${installationDetails.installedBy}`); @@ -71,7 +81,12 @@ function writeUserConf() { logger.info(`Arch : ${installationDetails.arch}`); logger.info(`Master node : ${masterNode}`); logger.info(`Unsafe sync : ${unsafeSync}`); - logger.info(`Proxies : ${TRUSTED_PROXYS}`); + logger.info(`Proxies : ${trustedProxies}`); + logger.info(`Log Level : ${LOG_LEVEL}`); + logger.info(`Server : http://localhost:${port}`); + if (process.env.NODE_ENV !== "production") { + logger.info(`Swagger-UI : http://localhost:${port}/api-docs`); + } logger.info("-----------------------------------------"); } diff --git a/src/config/initFiles.ts b/src/config/initFiles.ts index 008749c..7524907 100644 --- a/src/config/initFiles.ts +++ b/src/config/initFiles.ts @@ -3,6 +3,7 @@ import logger from "../utils/logger"; import { atomicWrite } from "../utils/atomicWrite"; const files = [ + { path: "./src/data/highAvailability.json", content: "{}" }, { path: "./src/data/password.json", content: JSON.stringify( diff --git a/src/config/stacks.ts b/src/config/stacks.ts new file mode 100644 index 0000000..def75dc --- /dev/null +++ b/src/config/stacks.ts @@ -0,0 +1,260 @@ +import logger from "../utils/logger"; +import fs from "fs"; +import path from "path"; +import YAML from "yamljs"; +import { DockerComposeFile } from "../typings/dockerCompose"; +import { dockerStackProperty, dockerStackEnv } from "../typings/dockerStackEnv"; +import { stackConfig } from "../typings/stackConfig"; +import { validate } from "../handlers/stack"; +import { atomicWrite } from "../utils/atomicWrite"; +import { AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT } from "./variables"; + +const nameRegex = /^[A-Za-z0-9_-]+$/; +const stackRootFolder = "./stacks"; +const configFilePath = `${stackRootFolder}/.config.json`; + +async function getStackCompose(name: string) { + try { + await validate(name); + const stackCompose = `${stackRootFolder}/${name}/docker-compose.yaml`; + + return YAML.parse(fs.readFileSync(stackCompose, "utf-8")); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + throw new Error(errorMsg); + } +} + +async function getStackConfig(): Promise { + try { + return fs.readFileSync(configFilePath, "utf-8"); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + throw new Error(errorMsg); + } +} + +async function createStack( + name: string, + content: DockerComposeFile, + override: boolean, +) { + try { + if (!name) { + const errorMsg = "Name required"; + logger.error(errorMsg); + throw new Error(errorMsg); + } + + if (!nameRegex.test(name)) { + const errorMsg = "Name does not match [A-Za-z0-9_-]"; + logger.error(errorMsg); + throw new Error(errorMsg); + } + + if (!content) { + const errorMsg = "Data for this stack is required"; + logger.error(errorMsg); + throw new Error(errorMsg); + } + + const stackFolderPath = `${stackRootFolder}/${name}`; + + if (!fs.existsSync(stackFolderPath)) { + fs.mkdirSync(stackFolderPath, { recursive: true }); + logger.debug(`Created stack folder at ${stackFolderPath}`); + } + + updateConfigFile(name); + + let yamlContent = ""; + let environmentFileData: dockerStackEnv = { environment: [] }; + if (AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT == "true" && override == false) { + logger.debug("AEFM is activated"); + const { cleanCompose, envSchema } = extractAndRemoveEnv(content); + yamlContent = YAML.stringify(cleanCompose, 10, 2); + environmentFileData = envSchema; + + await writeEnvFile(name, environmentFileData); + } else { + yamlContent = YAML.stringify(content, 10, 2); + } + + const filePath = `${stackFolderPath}/docker-compose.yaml`; + atomicWrite(filePath, yamlContent); + logger.debug(`Stack content written to ${filePath}`); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + throw new Error(errorMsg); + } +} + +function updateConfigFile(stackName: string) { + try { + let config: stackConfig = { stacks: [] }; + if (fs.existsSync(configFilePath)) { + const configData = fs.readFileSync(configFilePath, "utf-8"); + config = JSON.parse(configData); + } + + const stacks = config.stacks || []; + + if (!stacks.includes(stackName)) { + stacks.push(stackName); + } + + const updatedConfig = { stacks }; + atomicWrite(configFilePath, JSON.stringify(updatedConfig, null, 2)); + logger.debug(`Updated .config.json with stack name: ${stackName}`); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(`Error updating .config.json: ${errorMsg}`); + throw new Error(errorMsg); + } +} + +async function writeEnvFile( + name: string, + data: dockerStackEnv, +): Promise { + try { + await validate(name); + + if (!nameRegex.test(name)) { + const sanitizedStackName = name.replace(/\n|\r/g, ""); + const errorMsg = `Invalid stack name: ${sanitizedStackName}`; + logger.error(errorMsg); + return false; + } + + const dockerEnvPath = path.resolve(stackRootFolder, name, "docker.env"); + const dockerEnvPathBak = path.resolve( + stackRootFolder, + name, + ".docker.env.bak", + ); + + if ( + !dockerEnvPath.startsWith(path.resolve(stackRootFolder)) || + !dockerEnvPathBak.startsWith(path.resolve(stackRootFolder)) + ) { + const sanitizedStackName = name.replace(/\n|\r/g, ""); + const errorMsg = `Path traversal attempt detected: ${sanitizedStackName}`; + logger.error(errorMsg); + return false; + } + + const variableNames = data.environment.map(({ name }) => name); + const duplicateVars = variableNames.filter( + (item, index) => variableNames.indexOf(item) !== index, + ); + + if (duplicateVars.length > 0) { + const duplicatesList = duplicateVars.join(", "); + const sanitizedDuplicatesList = duplicatesList.replace(/\n|\r/g, ""); + const errorMsg = `Duplicate environment variables detected: ${sanitizedDuplicatesList}`; + logger.error(errorMsg); + return false; + } + + const envFileContent = data.environment + .map(({ name, value }) => `${name}="${value}"`) + .join("\n"); + + if (fs.existsSync(dockerEnvPath)) { + logger.debug("Creating a local backup"); + const previousData = fs.readFileSync(dockerEnvPath); + atomicWrite(dockerEnvPathBak, previousData); + } + + atomicWrite(dockerEnvPath, envFileContent); + return true; + } catch (error: unknown) { + const errorMsg = ( + error instanceof Error ? error.message : String(error) + ).replace(/\n|\r/g, ""); + logger.error(errorMsg); + throw new Error(errorMsg); + } +} + +async function getEnvFile(name: string) { + await validate(name); + const dockerEnvPath = path.resolve(stackRootFolder, name, "docker.env"); + if (!dockerEnvPath.startsWith(path.resolve(stackRootFolder))) { + throw new Error("Invalid path"); + } + + if (fs.existsSync(dockerEnvPath)) { + const data = fs.readFileSync(dockerEnvPath, "utf-8"); + + const environment: dockerStackProperty[] = data + .split("\n") + .filter((line) => line.trim() !== "" && line.includes("=")) + .map((line) => { + const [name, ...valueParts] = line.split("="); + const value = valueParts.join("=").replace(/^"|"$/g, ""); + return { name: name.trim(), value: value.trim() }; + }); + + return { environment }; + } else { + return null; + } +} + +function extractAndRemoveEnv(data: DockerComposeFile): { + cleanCompose: DockerComposeFile; + envSchema: dockerStackEnv; +} { + const environment: dockerStackProperty[] = []; + const envCount: Record = {}; + + for (const [, service] of Object.entries(data.services)) { + if (service.environment) { + for (const key of Object.keys(service.environment)) { + envCount[key] = (envCount[key] || 0) + 1; + } + } + } + + for (const [, service] of Object.entries(data.services)) { + if (service.environment) { + const remainingEnvironment: Record = {}; + + for (const [key, value] of Object.entries(service.environment)) { + if (envCount[key] === 1) { + environment.push({ name: key, value }); + } else { + remainingEnvironment[key] = value; + } + } + + service.environment = remainingEnvironment; + + if (Object.keys(service.environment).length === 0) { + delete service.environment; + } + } + + if (!service.env_file) { + service.env_file = ["./docker.env"]; + } + } + + return { + cleanCompose: data, + envSchema: { environment }, + }; +} + +export { + createStack, + getStackConfig, + getStackCompose, + writeEnvFile, + getEnvFile, +}; diff --git a/src/config/swagger.yaml b/src/config/swagger.yaml new file mode 100644 index 0000000..9a1d50f --- /dev/null +++ b/src/config/swagger.yaml @@ -0,0 +1,2095 @@ +openapi: "3.0.0" + +security: + - passwordAuth: [] + +info: + title: "DockStatAPI" + version: "2.0.1" + externalDocs: + description: DockStat(API) Wiki + url: https://outline.itsnik.de/s/dockstat + license: + name: BSD-3-Clause + url: https://github.com/Its4Nik/dockstatapi/tree/main?tab=BSD-3-Clause-1-ov-file#readme + contact: + email: info@itsnik.de + description: |- + ![DockStat](https://github.com/Its4Nik/dockstatapi/blob/dev/.github/DockStat-dark.png?raw=true) + + # Pipelines + + [![Docker Image CI](https://img.shields.io/github/actions/workflow/status/Its4Nik/dockstatapi/build-image.yml?branch=main&label=Docker%20Image%20CI&style=for-the-badge&logo=docker)](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-image.yml) + [![Validation](https://img.shields.io/github/actions/workflow/status/Its4Nik/dockstatapi/validation.yml?branch=dev&label=Validation&style=for-the-badge&logo=checkmarx)](https://github.com/Its4Nik/dockstatapi/actions/workflows/validation.yml) + + # Feature List: + + - Swagger API Documentation + - Database (Keeps data for 24 hours max) + - Advanced authentication using hashes and salt + - `http` API to configure the backend + - Multi-arch docker builds (using buildx github action) + - Advanced security through middlewares: rate-limiting and authentication + - Multi Arch Docker builds through docker buildx + - High Availability using single master and unlimited worker nodes! + +

+ Your container graph + [Interactive Graph](http://localhost:9876/graph) + + [Raw image](http://localhost:9876/graph/image) + + --- + + ![Your container graph](http://localhost:9876/graph/image) +
+ + # 🔗 DockStatAPI v2 Documentation + + _⚠️ = Deprecation warning_ + + - [Introduction](https://outline.itsnik.de/s/dockstat) + + - [DockstatAPI v2](https://outline.itsnik.de/s/dockstat/doc/dockstatapi-v2-XRMDKRqMIg) + + - [API reference](https://outline.itsnik.de/s/dockstat/doc/api-reference-1PTxqx1MQ6) + - [How dependency graphs are made](https://outline.itsnik.de/s/dockstat/doc/how-the-dependecy-graphs-are-made-svuZbEHH9g) + + - [DockStat v1](https://outline.itsnik.de/s/dockstat/doc/dockstat-v1-zVaFS4zROI) + + - [⚠️ Customisation](https://outline.itsnik.de/s/dockstat/doc/customization-PiBz4OpQIZ) + - [⚠️ Themes](https://outline.itsnik.de/s/dockstat/doc/themes-BFhN6ZBbYx) + - [⚠️ Installation](https://outline.itsnik.de/s/dockstat/doc/installation-DaO99bB86q) + + - [⚠️ DockStatAPI v1](https://outline.itsnik.de/s/dockstat/doc/dockstatapi-v1-jLcVCfPNmS) + - [⚠️ Integrations](https://outline.itsnik.de/s/dockstat/doc/integrations-Agq1oL6HxF) + - [⚠️ Backend API reference](https://outline.itsnik.de/s/dockstat/doc/backend-api-reference-YzcBbDvY33) + +tags: + - name: Authentication + description: Routes to setup / configure authentication + + - name: Configuration + description: Configuring the backend + + - name: Database queries + description: Queries made against the SQLite database + + - name: "Frontend Configuration" + description: Backend routes to configure the integrated "frontend service" + + - name: Miscellaneous + description: Some "random" routes which still can be useful + + - name: High availability + description: High availability routes, mainly used by HA sync + + - name: Notification Service + description: Routes to configure the notification service + + - name: Stacks + description: Management of the Stack module + +servers: + - url: http://localhost:9876 + description: "Your DockStatAPI instance" + +paths: + # ------------------------------ + # Authentication setup: + /auth/enable: + post: + tags: + - "Authentication" + summary: Enable authentication for every route + operationId: enableAuth + parameters: + - name: password + in: query + required: true + explode: true + schema: + type: string + default: super-secret + responses: + "200": + description: Success - Successfully enabled authentication + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + message: + type: string + example: "Authentication enabled successfully" + + "403": + description: Error - Password is required / Authentication is already enabled + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /auth/disable: + post: + tags: + - "Authentication" + summary: Disable authentication for every route + operationId: disableAuth + parameters: + - name: password + in: query + required: true + explode: true + schema: + type: string + default: super-secret + responses: + "200": + description: Succes - Succesfully disabled authentication + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + message: + type: string + example: "Authentication disabled successfully" + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + # ------------------------------ + # Database queries: + /data/latest: + get: + tags: + - "Database queries" + summary: Fetched the last added entry from the Database and provides it via a JSON output + operationId: getLatestData + responses: + "200": + description: Succes - Successfully fetched the database + content: + application/json: + schema: + $ref: "#/components/schemas/ServerContainers" + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "404": + description: Error - No entries found inside database + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /data/all: + get: + tags: + - "Database queries" + summary: Provides all database entries with an index starting from 0 + operationId: getAllData + responses: + "200": + description: Succes - Successfully fetched the database + content: + application/json: + schema: + $ref: "#/components/schemas/IndexedServerContainers" + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "404": + description: Error - No entries found inside database + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /data/clear: + delete: + tags: + - "Database queries" + summary: Deletes all database entries + operationId: dataClear + responses: + "200": + description: Succes - Successfully cleared the database + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + message: + type: string + example: "Successfully cleared the database" + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + # ------------------------------ + # Configuration: + /api/hosts: + get: + tags: + - "Configuration" + summary: Retrieves the configured name of all added Hosts + operationId: getHosts + responses: + "200": + description: Succes - Successfully fetched all configured hosts + content: + application/json: + schema: + type: array + example: '[ "Host-1", "Host-2" ]' + + "400": + description: Error - No hosts defined, please add a host via /conf/addHost + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /api/host/{hostName}/stats: + get: + tags: + - "Configuration" + summary: Shows general information about the target host, like dockeer engine version + operationId: getHostInfo + parameters: + - name: hostName + in: path + description: Hostname of the target host + required: true + schema: + type: string + responses: + "200": + description: Succes - Successfully fetched info about target host + content: + application/json: + schema: + $ref: "#/components/schemas/HostInfo" + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "404": + description: Error - No Host found + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /api/system: + get: + tags: + - "Configuration" + summary: Fetched the installation details of this DockStatAPI instance + operationId: getSystem + responses: + "200": + description: Succes - Fetched system configuration + content: + application/json: + schema: + type: object + properties: + installedAt: + type: string + format: date-time + example: "2024-12-25T19:20:02.418Z" + backendVersion: + type: string + example: "2.0.1" + inDocker: + type: boolean + example: false + installedBy: + type: string + example: "user" + platform: + type: string + example: "linux" + arch: + type: string + example: "x64" + "400": + description: Error - Received empty configuration + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /api/config: + get: + tags: + - "Configuration" + summary: Retrieves information about the configured hosts + operationId: getConfig + responses: + "200": + description: Succes - Fetched system configuration + content: + application/json: + schema: + type: object + properties: + hosts: + type: array + items: + type: object + properties: + name: + type: string + example: "Host-1" + url: + type: string + example: "192.168.2.12" + port: + type: string + example: "2375" + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /api/frontend-config: + get: + tags: + - "Configuration" + summary: Fetches the "Frontend Configuration" => Used in the DockStat frontend + operationId: getFrontendConfig + responses: + "200": + description: Succes - Fetched "Frontend Configuration" + content: + application/json: + schema: + $ref: "#/components/schemas/FrontendConfig" + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /api/current-schedule: + get: + tags: + - "Configuration" + summary: Shows the current configured schedule (for fetching data) in seconds + operationId: getSchedule + responses: + "200": + description: Succes - Fetched schedule + content: + application/json: + schema: + type: object + properties: + interval: + type: integer + example: 600 + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /api/status: + get: + tags: + - "Miscellaneous" + summary: Pings all hosts to check reachability + operationId: getStatus + responses: + "200": + description: Succes - Gathered Status + content: + application/json: + schema: + $ref: "#/components/schemas/ApiStatus" + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /api/containers: + get: + tags: + - "Miscellaneous" + summary: Fetched all container data directly from the host without reading from the database + operationId: getContainers + responses: + "200": + description: Succes - Fetched all container statistics + content: + application/json: + schema: + $ref: "#/components/schemas/ServerContainers" + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + # ------------------------------ + # High availability: + /ha/config: + get: + tags: + - "High availability" + summary: Get the current high availability config + operationId: getHaConfig + responses: + "200": + description: Succes - Fetched high availability config + content: + application/json: + schema: + $ref: "#/components/schemas/HaConfig" + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + + /ha/sync: + post: + tags: + - "High availability" + deprecated: true + summary: This route is not deprecated, but only used by the high availability feature + operationId: syncHa + responses: + "200": + description: Succes - Synchronized successfully + "400": + description: Error - `files` object is missing or invalid + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + + /ha/prepare-sync: + get: + tags: + - "High availability" + deprecated: true + summary: This route is not deprecated, but only used by the high availability feature + operationId: syncPrepare + responses: + "200": + description: Succes - Prepared all files for syncing + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + + # ------------------------------ + # Notification Service: + /notification-service/get-template: + get: + tags: + - "Notification Service" + summary: Fetches the current template for the notification service + operationId: getNsTemplate + responses: + "200": + description: Success - Fetched notification template + content: + application/json: + schema: + $ref: "#/components/schemas/Notification-Template" + "400": + description: Error - Error while reading file (see server logs) + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /notification-service/set-template: + post: + tags: + - "Notification Service" + - "Configuration" + summary: Update the current notification template + operationId: setNsTemplate + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Notification-Template" + responses: + "200": + description: Success - Template updated successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + message: + type: string + example: "Template updated successfully." + "400": + description: Error - Invalid input format. Expected JSON with a 'text' field + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "error" + message: + type: string + example: "Invalid input format. Expected JSON with a 'text' field" + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /notification-service/test/{type}/{containerId}: + post: + tags: + - "Notification Service" + summary: Test a specific type of notification using real data + operationId: testNs + parameters: + - in: path + name: type + required: true + schema: + type: string + description: The desired notification to test + + - in: path + name: containerId + required: true + schema: + type: string + description: A real container ID is needed to test templating functionality + responses: + "200": + description: Success - Sent test notification + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + message: + type: string + example: "Sent test notification" + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + # ------------------------------ + # Configuration: + /conf/addHost: + put: + tags: + - "Configuration" + summary: Adds a new host to the configuration and starts querying it + operationId: addHost + parameters: + - name: name + in: query + required: true + description: A name for the new host + - name: url + in: query + required: true + description: The target IP or dns entry + - name: port + in: query + required: true + description: The targets port on which Docker-Socket-Proxy runs + responses: + "200": + description: Success - Host added successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + message: + type: string + example: "Host added successfully" + "400": + description: Error - Name, Port, and URL are required + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "error" + message: + type: string + example: "Name, Port, and URL are required" + "401": + description: Host already exists + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "error" + message: + type: string + example: "Host already exists" + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /conf/removeHost: + delete: + tags: + - "Configuration" + summary: Removes an host from the config + operationId: removeHost + parameters: + - name: hostName + in: query + required: true + description: "The name of the to-be-removed-Host" + responses: + "200": + description: Success - Host removed successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + message: + type: string + example: "Host removed successfully" + "401": + description: Error - Host name is required + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "error" + message: + type: string + example: "Host name is required" + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "404": + description: Error - Host not found + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "error" + message: + type: string + example: "Host not found" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /conf/scheduler: + tags: + - "Configuration" + summary: Adjust the scheduler timing + operationId: adjustSchedule + parameters: + - name: interval + in: query + required: true + description: "Adjust the schedule timing (in seconds)" + responses: + "200": + description: Success - Timing updated + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + message: + type: string + example: "Updated interval" + "401": + description: Error - Interval must be between 5 minutes and 6 hours + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "error" + message: + type: string + example: "Interval must be between 5 minutes and 6 hours." + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + # ------------------------------ + # Frontend routes: + /frontend/show/{containerName}: + post: + tags: + - "Frontend Configuration" + operationId: frShowCon + summary: Set `hide` to false for the specified container + parameters: + - name: containerName + in: path + schema: + type: string + required: true + description: The name of the container to unhide + responses: + "200": + description: Success - now showing the container + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + message: + type: string + example: "Container unhidden successfully." + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /frontend/hide/{containerName}: + delete: + tags: + - "Frontend Configuration" + operationId: frHideCon + summary: Set `hide` to true for the specified container + parameters: + - name: containerName + in: path + schema: + type: string + required: true + description: The name of the container to unhide + responses: + "200": + description: Success - now hiding the container + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + message: + type: string + example: "Hid container succesfully" + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /frontend/tag/{containerName}/{tag}: + post: + tags: + - "Frontend Configuration" + operationId: frTagCon + summary: Add a tag to the tag array for the specified container + parameters: + - name: containerName + in: path + schema: + type: string + required: true + description: The name of the container to add a tag to + - name: tag + in: path + schema: + type: string + required: true + description: The name of the tag to add + responses: + "200": + description: Success - Tag added successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + message: + type: string + example: "Tag added successfully." + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /frontend/remove-tag/{containerName}/{tag}: + delete: + tags: + - "Frontend Configuration" + operationId: frRmTagCon + summary: Remove the specified tag from the tag array for the specified container + parameters: + - name: containerName + in: path + schema: + type: string + required: true + description: The name of the container to remove a tag from + - name: tag + in: path + schema: + type: string + required: true + description: The name of the tag to remove + responses: + "200": + description: Success - Tag removed successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + message: + type: string + example: "Tag removed successfully." + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /frontend/pin/{containerName}: + post: + tags: + - "Frontend Configuration" + operationId: frPinCon + summary: Set `pinned` to true for the specified container + parameters: + - name: containerName + in: path + schema: + type: string + required: true + description: The name of the container to pin + responses: + "200": + description: Success - Container pinned successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + message: + type: string + example: "Container pinned successfully." + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /frontend/unpin/{containerName}: + delete: + tags: + - "Frontend Configuration" + operationId: frRmPinCon + summary: Set `pinned` to false for the specified container + parameters: + - name: containerName + in: path + schema: + type: string + required: true + description: The name of the container to unpin + responses: + "200": + description: Success - Container unpinned successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + message: + type: string + example: "Container unpinned successfully." + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /frontend/add-link/{containerName}/{link}: + post: + tags: + - "Frontend Configuration" + operationId: frAddLinkCon + summary: Add a link to the specified container + parameters: + - name: containerName + in: path + schema: + type: string + required: true + description: The name of the container to add a link to + - name: link + in: path + schema: + type: URI + required: true + allowReserved: false + description: The URI of the link (please use Uniform Resource Identifier format) + responses: + "200": + description: Success - Link added to container successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + message: + type: string + example: "Link added successfully." + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /frontend/remove-link/{containerName}: + delete: + tags: + - "Frontend Configuration" + operationId: frRmLinkCon + summary: Remove a link to the specified container + parameters: + - name: containerName + in: path + schema: + type: string + required: true + description: The name of the container to remove a link from + responses: + "200": + description: Success - Link removed from container successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + message: + type: string + example: "Link removed successfully." + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /frontend/add-icon/{containerName}/{icon}/{useCustomIcon}: + post: + tags: + - "Frontend Configuration" + operationId: frAddIcon + summary: Add an icon (path) to the specified container + parameters: + - name: containerName + in: path + schema: + type: string + required: true + description: The name of the container to add an icon to + - name: icon + in: path + schema: + type: string + required: true + description: The name of the icon file + - name: useCustomIcon + in: path + schema: + type: boolean + required: false + description: If the icon is a custom icon or not + responses: + "200": + description: Success - Icon added to container successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + message: + type: string + example: "Icon added successfully." + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /frontend/remove-icon/{containerName}: + delete: + tags: + - "Frontend Configuration" + operationId: frRmIcon + summary: Remove an icon from the specified container + parameters: + - name: containerName + in: path + schema: + type: string + required: true + description: The name of the container to remove an icon from + responses: + "200": + description: Success - Icon removed from container successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + message: + type: string + example: "Icon removed successfully." + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + # ------------------------------ + # Stack management + /stacks/create/{name}: + post: + tags: + - "Stacks" + operationId: createStack + summary: Creates a docker-compose file inside the stack name directory + requestBody: + required: true + content: + application/json: + schema: + type: string + description: Your docker-compose.yaml contents + parameters: + - name: name + in: path + schema: + type: string + required: true + description: The name of the stack + responses: + "200": + description: Success - Stack created + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + message: + type: string + example: "Stack created" + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /stacks/start/{name}: + post: + tags: + - "Stacks" + operationId: startStack + summary: Starts the defined stack + parameters: + - name: name + in: path + schema: + type: string + required: true + description: The name of the stack + responses: + "200": + description: Success - Stack started + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + message: + type: string + example: "Stack created" + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /stacks/stop/{name}: + post: + tags: + - "Stacks" + operationId: stopStack + summary: Stops the defined stack + parameters: + - name: name + in: path + schema: + type: string + required: true + description: The name of the stack + responses: + "200": + description: Success - Stack stopped + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + message: + type: string + example: "Stack stopped" + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /stacks/get/{name}: + get: + tags: + - "Stacks" + operationId: getStack + summary: Get the docker-compose.yaml (as JSON) from the defined stack + parameters: + - name: name + in: path + schema: + type: string + required: true + description: The name of the stack + responses: + "200": + description: Success - Stack fetched + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /stacks/set-env/{name}: + post: + tags: + - "Stacks" + operationId: setStackEnv + summary: Set the docker.env (as JSON) from the defined stack + requestBody: + required: true + content: + application/json: + schema: + type: string + description: Your docker.env contents + parameters: + - name: override + in: query + required: false + description: Whether to override (true) the automatic environment file management (boolean value) + - name: name + in: path + schema: + type: string + required: true + description: The name of the stack + responses: + "200": + description: Success - Stack environment set + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /stacks/get-env/{name}: + get: + tags: + - "Stacks" + operationId: getStackEnv + summary: Get the docker.env (as JSON) from the defined stack + parameters: + - name: name + in: path + schema: + type: string + required: true + description: The name of the stack + responses: + "200": + description: Success - Stack config fetched + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + +# ------------------------------ +components: + securitySchemes: + passwordAuth: + type: apiKey + in: header + name: x-password + description: Password required for authentication + + schemas: + Notification-Template: + type: object + properties: + text: + type: string + example: "{{container}} on {{host}} is {{state}}" + + IndexedServerContainers: + type: object + properties: + "0": + type: object + properties: + Host-1: + type: array + items: + $ref: "#/components/schemas/Container" + additionalProperties: false + + ServerContainers: + type: object + properties: + Host-1: + type: array + items: + $ref: "#/components/schemas/Container" + additionalProperties: false + + Container: + type: object + properties: + name: + type: string + description: The name of the container. + example: "Container-1" + id: + type: string + description: The unique identifier of the container. + example: "a84ca83bb0e7f8c24fe472b9164d40a4bae518ece8369e6776f722b81dd65bcf" + hostName: + type: string + description: The hostname of the server. + example: "Host-1" + state: + type: string + description: The current state of the container. + example: "running" + cpu_usage: + type: number + description: The CPU usage of the container in arbitrary units. + example: 625185.1851851852 + mem_usage: + type: integer + description: Memory usage in bytes. + example: 359899136 + mem_limit: + type: integer + description: Memory limit in bytes. + example: 8127893504 + net_rx: + type: integer + description: Total network received in bytes. + example: 11004185462 + net_tx: + type: integer + description: Total network transmitted in bytes. + example: 9950013623 + current_net_rx: + type: integer + description: Current network received in bytes. + example: 11004185462 + current_net_tx: + type: integer + description: Current network transmitted in bytes. + example: 9950013623 + networkMode: + type: string + description: The network mode of the container. + example: "docker_default" + + HostInfo: + type: object + properties: + hostName: + type: string + example: "Host-1" + info: + type: object + properties: + ID: + type: string + format: uuid + example: "32b5fad9-9b12-48b0-9ce7-178f2886ad60" + Containers: + type: integer + example: 8 + ContainersRunning: + type: integer + example: 8 + ContainersPaused: + type: integer + example: 0 + ContainersStopped: + type: integer + example: 0 + Images: + type: integer + example: 7 + OperatingSystem: + type: string + example: "Ubuntu 24.04 LTS" + KernelVersion: + type: string + example: "6.8.0-38-generic" + Architecture: + type: string + example: "x86_64" + MemTotal: + type: integer + example: 8127893504 + NCPU: + type: integer + example: 4 + version: + type: object + properties: + Components: + type: object + properties: + Engine: + type: string + example: "27.1.1" + containerd: + type: string + example: "1.7.19" + runc: + type: string + example: "1.7.19" + docker-init: + type: string + example: "0.19.0" + + Frontend: + type: object + properties: + name: + type: string + description: The name of the container + hidden: + type: boolean + description: Whether the container is hidden + tags: + type: array + items: + type: string + description: List of tags associated with the container + link: + type: string + format: uri + description: A link associated with the container + icon: + type: string + description: Icon for the container + pinned: + type: boolean + description: Whether the container is pinned + required: + - name + + FrontendConfig: + type: array + items: + $ref: "#/components/schemas/Frontend" + + ApiStatus: + type: object + properties: + ApiReachable: + type: boolean + description: Whether the API is reachable + online: + type: object + description: Status of individual services keyed by their names + properties: + Host-1: + type: boolean + Host-2: + type: boolean + required: + - ApiReachable + - online + + HaConfig: + type: object + properties: + active: + type: boolean + description: Whether High availability is active or nots + master: + type: boolean + description: Whether this node is the master node + nodes: + type: array + items: + type: string + format: hostname + description: List of nodes in the cluster, specified by hostname or IP with port + required: + - active + - master + - nodes + + 401: + type: object + properties: + status: + type: string + example: "error" + message: + type: string + example: "Invalid password" + + 403: + type: object + properties: + status: + type: string + example: "denied" + message: + type: string + example: "Password required" + + 500: + type: object + properties: + status: + type: string + example: "critical" + message: + type: string + example: "Please see the server logs for more info" + + 503: + type: object + properties: + status: + type: string + example: "error" + message: + type: string + example: "Service unavailable. The high-availability lock is currently active. Please try again later." diff --git a/src/config/swaggerConfig.ts b/src/config/swaggerConfig.ts index cab967f..39c074a 100644 --- a/src/config/swaggerConfig.ts +++ b/src/config/swaggerConfig.ts @@ -1,53 +1,10 @@ -const options: { - definition: { - failOnErrors: boolean; - openapi: string; - info: { - title: string; - version: string; - description: string; - }; - components: { - securitySchemes: { - passwordAuth: { - type: string; - in: string; - name: string; - description: string; - }; - }; - }; - security: Array<{ - passwordAuth: unknown[]; - }>; - }; - apis: string[]; -} = { - definition: { - failOnErrors: true, - openapi: "3.0.0", - info: { - title: "DockStatAPI", - version: "2", - description: "An API used to query muliple docker hosts", - }, - components: { - securitySchemes: { - passwordAuth: { - type: "apiKey", - in: "header", - name: "x-password", - description: "Password required for authentication", - }, - }, - }, - security: [ - { - passwordAuth: [], - }, - ], +import { SwaggerOptions } from "swagger-ui-express"; +import { css } from "./swaggerTheme"; + +export const options: SwaggerOptions = { + swaggerOptions: { + tryItOutEnabled: true, }, - apis: ["./src/routes/*/*.ts"], + customCss: css, + explorer: false, }; - -export default options; diff --git a/src/config/swaggerTheme.ts b/src/config/swaggerTheme.ts new file mode 100644 index 0000000..d8a879c --- /dev/null +++ b/src/config/swaggerTheme.ts @@ -0,0 +1,6 @@ +export const css = ` + +.swagger-ui .topbar { + display: none +} +`; diff --git a/src/config/variables.ts b/src/config/variables.ts index 26a522b..37c67a2 100644 --- a/src/config/variables.ts +++ b/src/config/variables.ts @@ -3,7 +3,7 @@ import vars from "../data/variables.json"; export const { VERSION, RUNNING_IN_DOCKER, - TRUSTED_PROXYS, + TRUSTED_PROXIES, HA_MASTER, HA_MASTER_IP, HA_NODE, @@ -21,4 +21,6 @@ export const { TELEGRAM_CHAT_ID, WHATSAPP_API_URL, WHATSAPP_RECIPIENT, + AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT, + LOG_LEVEL, } = vars; diff --git a/src/controllers/containerController.ts b/src/controllers/containerController.ts index 61745e1..2883dad 100644 --- a/src/controllers/containerController.ts +++ b/src/controllers/containerController.ts @@ -1,4 +1,4 @@ -import getDockerClient from "../utils/dockerClient"; +import { getDockerClient } from "../utils/dockerClient"; import logger from "../utils/logger"; import { Request, Response } from "express"; import { createResponseHandler } from "../handlers/response"; diff --git a/src/controllers/fetchData.ts b/src/controllers/fetchData.ts index 07438ec..06e52a9 100644 --- a/src/controllers/fetchData.ts +++ b/src/controllers/fetchData.ts @@ -1,5 +1,5 @@ import db from "../config/db"; -import fetchAllContainers from "../utils/containerService"; +import { fetchAllContainers } from "../utils/containerService"; import logger from "../utils/logger"; import fs from "fs"; import { atomicWrite } from "../utils/atomicWrite"; @@ -68,9 +68,8 @@ const fetchData = async (): Promise => { logger.info("No state change detected, notifications not triggered."); } } catch (error: unknown) { - logger.error( - `Error fetching data: ${JSON.stringify(error)} \nStack trace: ${(error as Error).stack}`, - ); + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } }; diff --git a/src/controllers/frontendConfiguration.ts b/src/controllers/frontendConfiguration.ts index e8e035c..ed4e59d 100644 --- a/src/controllers/frontendConfiguration.ts +++ b/src/controllers/frontendConfiguration.ts @@ -23,8 +23,8 @@ async function hideContainer(containerName: string) { await saveData(data); } } catch (error: unknown) { - logger.error(error as Error); - throw new Error(error as string); + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } } @@ -41,8 +41,8 @@ async function unhideContainer(containerName: string) { cleanupData(); } } catch (error: unknown) { - logger.error(error as Error); - throw new Error(error as string); + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } } @@ -66,8 +66,8 @@ async function addTagToContainer(containerName: string, tag: string) { await saveData(data); } } catch (error: unknown) { - logger.error(error as Error); - throw new Error(error as string); + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } } @@ -86,8 +86,8 @@ async function removeTagFromContainer(containerName: string, tag: string) { cleanupData(); } } catch (error: unknown) { - logger.error(error); - throw new Error(error as string); + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } } @@ -108,8 +108,8 @@ async function pinContainer(containerName: string) { await saveData(data); } } catch (error: unknown) { - logger.error(error as Error); - throw new Error(error as string); + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } } @@ -126,8 +126,8 @@ async function unpinContainer(containerName: string) { cleanupData(); } } catch (error: unknown) { - logger.error(error as Error); - throw new Error(error as string); + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } } @@ -149,8 +149,8 @@ async function setLink(containerName: string, link: string) { await saveData(data); } } catch (error: unknown) { - logger.error(error); - throw new Error(error as string); + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } } else { logger.error(`Provided link is not valid: ${link}`); @@ -171,8 +171,8 @@ async function removeLink(containerName: string) { cleanupData(); } } catch (error: unknown) { - logger.error(error as Error); - throw new Error(error as string); + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } } @@ -201,8 +201,8 @@ async function setIcon(containerName: string, icon: string, custom: boolean) { await saveData(data); } } catch (error: unknown) { - logger.error(error as Error); - throw new Error(error as string); + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } } @@ -219,8 +219,8 @@ async function removeIcon(containerName: string) { cleanupData(); } } catch (error: unknown) { - logger.error(error as Error); - throw new Error(error as string); + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } } @@ -252,7 +252,8 @@ async function saveData(data: FrontendConfig) { ); logger.info("Succesfully wrote to file"); } catch (error: unknown) { - logger.error(error as Error); + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } } @@ -277,7 +278,8 @@ async function cleanupData() { await saveData(cleanedData); } catch (error: unknown) { - logger.error(error as Error); + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } } diff --git a/src/controllers/highAvailability.ts b/src/controllers/highAvailability.ts index 3e61b16..45db9d7 100644 --- a/src/controllers/highAvailability.ts +++ b/src/controllers/highAvailability.ts @@ -57,9 +57,9 @@ async function acquireLock(): Promise { try { atomicWrite(lockFilePath, "locked", { exclusive: true }); logger.debug("Lock acquired."); - } catch (error) { - logger.error(`Error acquiring lock: ${(error as Error).message}`); - throw new Error("Failed to acquire lock."); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } } @@ -69,8 +69,9 @@ async function releaseLock(): Promise { await fs.promises.unlink(lockFilePath); logger.debug("Lock released."); } - } catch (error) { - logger.error(`Error releasing lock: ${(error as Error).message}`); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } } @@ -88,8 +89,9 @@ async function writeConfig( await fs.promises.writeFile(filePath, jsonData); logger.debug(`${filePath} has been written.`); - } catch (error) { - logger.error(`Error writing config: ${(error as Error).message}`); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } finally { await releaseLock(); } @@ -104,7 +106,8 @@ async function readConfig(): Promise { ); return data; } catch (error: unknown) { - logger.error(`Error reading HA-Config: ${(error as Error).message}`); + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); return null; } finally { await releaseLock(); @@ -118,8 +121,9 @@ async function prepareFilesForSync(): Promise> { const content = await fs.promises.readFile(filePath, "utf-8"); fileData[filePath] = content; } - } catch (error) { - logger.error(`Error preparing files for sync: ${(error as Error).message}`); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } return fileData; } @@ -147,8 +151,9 @@ async function checkApiReachable(node: string): Promise { logger.error(`Node ${node} is not reachable. ApiReachable: false`); return false; } - } catch (error) { - logger.error(`Error reaching node ${node}: ${(error as Error).message}`); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); return false; } } @@ -229,7 +234,7 @@ async function startMasterNode() { ? HA_NODE.split(",").reduce((cache, node, index) => { const [ip, port] = node.trim().split(":"); if (ip && port) { - cache[`node-${index + 1}`] = { ip, id: parseInt(port, 10) }; + cache[`node-${index + 1}`] = { ip, port: parseInt(port, 10) }; } return cache; }, {} as NodeCache) @@ -260,10 +265,9 @@ async function ensureFileExists( await fs.promises.mkdir(dirPath, { recursive: true }); await fs.promises.writeFile(filePath, content, { flag: "w" }); logger.info(`File updated: ${filePath}`); - } catch (error) { - logger.error( - `Error creating/updating file ${filePath}: ${(error as Error).message}`, - ); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } finally { await releaseLock(); } diff --git a/src/controllers/proxy.ts b/src/controllers/proxy.ts index 601f155..c091590 100644 --- a/src/controllers/proxy.ts +++ b/src/controllers/proxy.ts @@ -1,9 +1,9 @@ import { Application } from "express"; import logger from "../utils/logger"; -import { TRUSTED_PROXYS } from "../config/variables"; +import { TRUSTED_PROXIES } from "../config/variables"; export default function trustedProxies(app: Application) { - const trusted: string = TRUSTED_PROXYS; + const trusted: string = TRUSTED_PROXIES; if (!trusted) { logger.warn( diff --git a/src/controllers/scheduler.ts b/src/controllers/scheduler.ts index caa1948..db450d9 100644 --- a/src/controllers/scheduler.ts +++ b/src/controllers/scheduler.ts @@ -12,7 +12,8 @@ const scheduleFetch = () => { fetchData(); cleanupOldEntries(); } catch (error: unknown) { - logger.error(`Error during scheduled fetch: ${error}`); + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } intervalId = setInterval(() => { @@ -81,8 +82,9 @@ const cleanupOldEntries = async () => { try { db.run("DELETE FROM data WHERE timestamp < ?", twentyFourHoursAgo, Error); logger.info("Old entries cleared from the database."); - } catch (Error: unknown) { - logger.error(`Error clearing old entries: ${(Error as Error).message}`); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } }; diff --git a/src/data/frontendConfiguration.json b/src/data/frontendConfiguration.json index fe51488..0637a08 100644 --- a/src/data/frontendConfiguration.json +++ b/src/data/frontendConfiguration.json @@ -1 +1 @@ -[] +[] \ No newline at end of file diff --git a/src/handlers/api.ts b/src/handlers/api.ts index 6f62c05..fa7f1f7 100644 --- a/src/handlers/api.ts +++ b/src/handlers/api.ts @@ -1,7 +1,7 @@ import extractRelevantData from "../utils/extractHostData"; import { Request, Response } from "express"; -import getDockerClient from "../utils/dockerClient"; -import fetchAllContainers from "../utils/containerService"; +import { getDockerClient } from "../utils/dockerClient"; +import { fetchAllContainers } from "../utils/containerService"; import { getCurrentSchedule } from "../controllers/scheduler"; import fs from "fs"; import checkReachability from "../utils/connectionChecker"; @@ -62,6 +62,10 @@ class ApiHandler { const version = await docker.version(); const relevantData = extractRelevantData({ hostName, info, version }); + if (!relevantData) { + ResponseHandler.error("No host found", 404); + } + return ResponseHandler.rawData(relevantData, "Fetched Host stats"); } catch (error: unknown) { const errorMsg = error instanceof Error ? error.message : String(error); diff --git a/src/handlers/conf.ts b/src/handlers/conf.ts index e383c4d..b49dd2a 100644 --- a/src/handlers/conf.ts +++ b/src/handlers/conf.ts @@ -20,7 +20,7 @@ class ConfHandler { try { const { name, url, port } = req.query as unknown as target; if (!name || !url || !port) { - return ResponseHandler.denied("Name, Port, and URL are required."); + return ResponseHandler.error("Name, Port, and URL are required.", 400); } const config: dockerConfig = JSON.parse( @@ -28,7 +28,7 @@ class ConfHandler { ); if (config.hosts.some((host) => host.name === name)) { - return ResponseHandler.denied("Host already exists."); + return ResponseHandler.error("Host already exists.", 422); } config.hosts.push({ name, url, port }); @@ -47,7 +47,7 @@ class ConfHandler { const hostName = req.query.hostName as string; if (!hostName) { - return ResponseHandler.denied("Host name is required."); + return ResponseHandler.error("Host name is required.", 401); } const currentState = fs.readFileSync(configPath, "utf-8"); @@ -79,8 +79,9 @@ class ConfHandler { const newInterval = parseInterval(interval); if (newInterval < 5 * 60 * 1000 || newInterval > 6 * 60 * 60 * 1000) { - return ResponseHandler.denied( + return ResponseHandler.error( "Interval must be between 5 minutes and 6 hours.", + 401, ); } diff --git a/src/handlers/data.ts b/src/handlers/data.ts index fd3515d..5d3bf41 100644 --- a/src/handlers/data.ts +++ b/src/handlers/data.ts @@ -2,6 +2,7 @@ import { Response, Request } from "express"; import db from "../config/db"; import { Table, DataRow } from "../typings/table"; import { createResponseHandler } from "./response"; +import logger from "../utils/logger"; function formatRows(rows: DataRow[]): Record { return rows.reduce( @@ -56,6 +57,35 @@ class DatabaseHandler { ); } + latestRaw(): Promise { + return new Promise((resolve, reject) => { + logger.debug("Reading DB"); + db.get( + "SELECT info FROM data ORDER BY timestamp DESC LIMIT 1", + (error: unknown, row: Partial> | undefined) => { + if (error) { + return reject(`Database query error: ${error}`); + } + + if (!row || !row.info) { + return reject("No data available for /data/latest"); + } + + try { + logger.info("Read latest data"); + const parsedData = JSON.parse(row.info); + logger.debug("Parsed data:", parsedData); + resolve(parsedData); + } catch (error: unknown) { + const errorMsg = + error instanceof Error ? error.message : String(error); + reject(`Error parsing data: ${errorMsg}`); + } + }, + ); + }); + } + all() { const ResponseHandler = createResponseHandler(this.res); const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); diff --git a/src/handlers/graph.ts b/src/handlers/graph.ts new file mode 100644 index 0000000..53e245f --- /dev/null +++ b/src/handlers/graph.ts @@ -0,0 +1,257 @@ +import cytoscape from "cytoscape"; +import logger from "../utils/logger"; +import { AllContainerData, ContainerData } from "./../typings/dockerConfig"; +import { atomicWrite } from "../utils/atomicWrite"; +import { rateLimitedReadFile } from "../utils/rateLimitFS"; + +const CACHE_DIR_JSON = "./src/data/graph.json"; +const CACHE_DIR_HTML = "./src/data/graph.html"; +const _assets = "./src/utils/assets"; +const serverSvg = `${_assets}/server-icon.svg`; +const containerSvg = `${_assets}/container-icon.svg`; +const pngPath = "./src/data/graph.png"; + +async function getPathData(path: string) { + try { + return await rateLimitedReadFile(path); + + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + return false; + } +} + +async function renderGraphToImage( + htmlContent: string, + outputImagePath: string, +): Promise { + let puppeteer; + try { + puppeteer = await import("puppeteer"); + } catch (error) { + logger.error("Puppeteer is not installed. Please install it to generate images."); + throw new Error(`Puppeteer is not installed (${error})`); + } + + let browser; + try { + browser = await puppeteer.default.launch({ + headless: "shell", + args: ["--disable-setuid-sandbox", "--no-sandbox"], + executablePath: process.env.PUPPETEER_EXECUTABLE_PATH, + }); + + const page = await browser.newPage(); + await page.setContent(htmlContent, { waitUntil: "networkidle0" }); + await page.waitForSelector("#cy", { visible: true, timeout: 15000 }); + + await page.waitForFunction( + () => { + const cyElement = document.querySelector("#cy"); + return cyElement ? cyElement.children.length > 0 : false; + }, + { timeout: 10000 } + ); + + await page.screenshot({ + path: outputImagePath, + type: outputImagePath.endsWith(".jpg") ? "jpeg" : "png", + fullPage: true, + captureBeyondViewport: true, + }); + } catch (error: unknown) { + let errorMessage = "Unknown error occurred during browser operation"; + + if (error instanceof Error) { + errorMessage = error.message; + + // Detect common dependency errors + if (errorMessage.includes("libnss3") || errorMessage.includes("libxcb")) { + errorMessage = `❗ Missing system dependencies (libnss3)`; + } + + // Detect Chrome not found errors + if (errorMessage.includes("Failed to launch")) { + errorMessage = `❗ Chrome not found!`; + } + } + + throw new Error(`Graph rendering failed: ${errorMessage}`); + } finally { + if (browser) { + await browser.close().catch(() => { }); + } + } + + logger.info(`Graph rendered and image saved to: ${outputImagePath}`); +} + +async function generateGraphFiles( + allContainerData: AllContainerData, +): Promise { + if (process.env.CI === "true") { + logger.warn("Running inside a CI/CD Action, wont generated graphs"); + return false; + } else { + try { + logger.info("generateGraphFiles >>> Starting generation"); + const graphElements: cytoscape.ElementDefinition[] = []; + + for (const [hostName, containers] of Object.entries(allContainerData)) { + if ("error" in containers) { + // TODO: make error'ed hosts better + graphElements.push({ + data: { + id: hostName, + label: `Host: ${hostName} Error: ${containers.error}`, + type: "server", + }, + }); + } else { + const containerList = containers as ContainerData[]; + + // host node with container count + graphElements.push({ + data: { + id: hostName, + label: `${hostName} - ${containerList.length} Containers`, + type: "server", + }, + }); + + for (const container of containerList) { + // container node + graphElements.push({ + data: { + id: container.id, + label: `${container.name} (${container.state})`, + type: "container", + }, + }); + + // edge between host and container + graphElements.push({ + data: { + source: hostName, + target: container.id, + }, + }); + } + } + } + + atomicWrite(CACHE_DIR_JSON, JSON.stringify(graphElements, null, 2)); + + const htmlContent = ` + + + + + + Cytoscape Graph + + + + +
+ + + + `; + + atomicWrite(CACHE_DIR_HTML, htmlContent); + await renderGraphToImage(htmlContent, pngPath) + .then(() => logger.debug("HTML converted to image successfully!")) + .catch((err) => logger.error("Error:", err)); + + logger.info("generateGraphFiles <<< Files generated successfully"); + return true; + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + return false; + } + } +} + +function getGraphFilePaths() { + return { json: CACHE_DIR_JSON, html: CACHE_DIR_HTML }; +} + +export { generateGraphFiles, getGraphFilePaths }; diff --git a/src/handlers/notification.ts b/src/handlers/notification.ts index ad5c293..9c10a59 100644 --- a/src/handlers/notification.ts +++ b/src/handlers/notification.ts @@ -27,7 +27,10 @@ class NotificationHandler { if (error) { return ResponseHandler.error(error as string, 400); } - return ResponseHandler.rawData(data, "Fetched notification template"); + return ResponseHandler.rawData( + JSON.parse(data), + "Fetched notification template", + ); }); } catch (error: unknown) { const errorMsg = error instanceof Error ? error.message : String(error); diff --git a/src/handlers/response.ts b/src/handlers/response.ts index 8c6e95b..ee06210 100644 --- a/src/handlers/response.ts +++ b/src/handlers/response.ts @@ -29,7 +29,7 @@ class ResponseHandler { } critical(log: string) { - logger.error(log); + logger.error(log.replace(/\n|\r/g, "")); this.res.status(500).json({ status: "critical", message: "Please see the server logs for more info", diff --git a/src/handlers/stack.ts b/src/handlers/stack.ts new file mode 100644 index 0000000..e87b533 --- /dev/null +++ b/src/handlers/stack.ts @@ -0,0 +1,162 @@ +import { Response, Request } from "express"; +import { + createStack, + getStackConfig, + getStackCompose, + writeEnvFile, + getEnvFile, +} from "../config/stacks"; +import { DockerComposeFile } from "../typings/dockerCompose"; +import logger from "../utils/logger"; +import * as compose from "docker-compose"; +import { createResponseHandler } from "./response"; +import { stackConfig } from "../typings/stackConfig"; +import { dockerStackEnv } from "../typings/dockerStackEnv"; +import path from "path"; + +const PROJECT_ROOT = path.resolve(__dirname, "../.."); + +export async function validate(name: string): Promise { + const config: stackConfig = JSON.parse(await getStackConfig()); + if (!config.stacks.find((element) => element === name)) { + throw new Error("Stack not found"); + } + + return true; +} + +async function composeAction(option: string, name: string): Promise { + const composeFile: string = path.join(PROJECT_ROOT, `stacks/${name}`); + switch (option) { + case "start": { + await compose.upAll({ cwd: composeFile, log: false }).then( + () => { + return true; + }, + (err: unknown) => { + throw new Error(err as string); + }, + ); + break; + } + case "stop": { + await compose.downAll({ cwd: composeFile, log: false }).then( + () => { + return true; + }, + (err: unknown) => { + throw new Error(err as string); + }, + ); + break; + } + } +} + +class StackHandler { + private req: Request; + private res: Response; + + constructor(req: Request, res: Response) { + this.req = req; + this.res = res; + } + + async createStack(req: Request, res: Response) { + const ResponseHandler = createResponseHandler(res); + try { + const name: string = req.params.name; + const content: DockerComposeFile = req.body; + let override = false; + override = req.query.override == "true"; + + await createStack(name, content, override); + return ResponseHandler.ok("Stack created"); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + async start(req: Request, res: Response) { + const ResponseHandler = createResponseHandler(res); + try { + const name: string = req.params.name; + await validate(name); + await composeAction("start", name); + return ResponseHandler.ok("Stack started"); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + async stop(req: Request, res: Response) { + const ResponseHandler = createResponseHandler(res); + try { + const name: string = req.params.name; + await validate(name); + await composeAction("stop", name); + return ResponseHandler.ok("Stack stopped"); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + async stackCompose(req: Request, res: Response) { + const ResponseHandler = createResponseHandler(res); + try { + const {name} = req.params; + return ResponseHandler.rawData( + await getStackCompose(name), + "Stack compose fetched", + ); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg.replace(/\n|\r/g, "")); + throw new Error(errorMsg); + } + } + + async setStackEnv(req: Request, res: Response) { + const ResponseHandler = createResponseHandler(res); + try { + const data: dockerStackEnv = req.body; + const name: string = req.params.name; + if (await writeEnvFile(name, data)) { + return ResponseHandler.ok("Wrote docker.env"); + } else { + return ResponseHandler.critical( + "Something went wrong while writing the env File!", + ); + } + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg.replace(/\n|\r/g, "")); + throw new Error(errorMsg); + } + } + + async getStackEnv(req: Request, res: Response) { + const ResponseHandler = createResponseHandler(res); + try { + const name: string = req.params.name; + const data = await getEnvFile(name); + if (data == null) { + return ResponseHandler.error( + "No environment file found for this Stack!", + 404, + ); + } + return ResponseHandler.rawData(data, "Read docker.env"); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg.replace(/\n|\r/g, "")); + throw new Error(errorMsg); + } + } +} + +export const createStackHandler = (req: Request, res: Response) => + new StackHandler(req, res); diff --git a/src/init.ts b/src/init.ts index 8c75737..188542f 100644 --- a/src/init.ts +++ b/src/init.ts @@ -7,27 +7,47 @@ import frontend from "./routes/frontendController/routes"; import api from "./routes/getter/routes"; import notificationService from "./routes/notifications/routes"; import conf from "./routes/setter/routes"; +import graph from "./routes/graphs/routes"; import authMiddleware from "./middleware/authMiddleware"; import ha from "./routes/highavailability/routes"; import trustedProxies from "./controllers/proxy"; import { limiter } from "./middleware/rateLimiter"; import { scheduleFetch } from "./controllers/scheduler"; +import { Server } from 'http'; import cors from "cors"; +import { setupWebSocket } from "./utils/webSocket"; +import stacks from "./routes/stack/routes"; import { blockWhileLocked } from "./middleware/checkLock"; import logger from "./utils/logger"; import initFiles from "./config/initFiles"; const LAB = [limiter, authMiddleware, blockWhileLocked]; -const initializeApp = (app: express.Application): void => { +const initializeApp = (app: express.Application, server: Server): void => { initFiles(); + + try { + logger.debug("Starting Websocket server, with these endpoints:"); + logger.debug("ws://localhost:9876/wss/container-data") + logger.debug("ws://localhost:9876/wss/server-logs") + setupWebSocket(server); + } catch (error: unknown) { + logger.error("Error starting WebSocket: ", error) + } + app.use(cors()); app.use(express.json()); - app.use("/api-docs", (req: Request, res: Response, next: NextFunction) => - next(), - ); - swaggerDocs(app); + if (process.env.NODE_ENV !== "production") { + app.use("/api-docs", (req: Request, res: Response, next: NextFunction) => + next(), + ); + app.get("/", (req: Request, res: Response) => { + res.redirect("/api-docs"); + }); + swaggerDocs(app); + } + trustedProxies(app); scheduleFetch(); @@ -36,13 +56,11 @@ const initializeApp = (app: express.Application): void => { app.use("/auth", LAB, auth); app.use("/data", LAB, data); app.use("/frontend", LAB, frontend); + app.use("/graph", LAB, graph); app.use("/notification-service", LAB, notificationService); + app.use("/stacks", LAB, stacks); app.use("/ha", limiter, authMiddleware, ha); - app.get("/", (req: Request, res: Response) => { - res.redirect("/api-docs"); - }); - process.on("exit", (code: number) => { logger.warn(`Server exiting (Code: ${code})`); }); diff --git a/src/middleware/authMiddleware.ts b/src/middleware/authMiddleware.ts index 4afb393..414b276 100644 --- a/src/middleware/authMiddleware.ts +++ b/src/middleware/authMiddleware.ts @@ -36,7 +36,7 @@ async function authMiddleware( storedData.hash, ); if (!passwordMatch) { - ResponseHandler.denied("Invalid Password"); + ResponseHandler.error("Invalid Password", 402); return; } diff --git a/src/misc/createEnvDev.sh b/src/misc/createEnvDev.sh index 4a5a0bb..1f231aa 100755 --- a/src/misc/createEnvDev.sh +++ b/src/misc/createEnvDev.sh @@ -3,6 +3,9 @@ # Version VERSION="$(cat ./package.json | grep version | cut -d '"' -f 4)" +# Automatic Stack environment management +AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT="${AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT:-true}" + # Docker if grep -q '/docker' /proc/1/cgroup 2>/dev/null || [ -f /.dockerenv ]; then RUNNING_IN_DOCKER="true" @@ -10,11 +13,14 @@ else RUNNING_IN_DOCKER="false" fi +# Default dev log level +LOG_LEVEL="${LOG_LEVEL:-debug}" + echo -n "\ { \"VERSION\": \"${VERSION}\", \"RUNNING_IN_DOCKER\": \"${RUNNING_IN_DOCKER}\", - \"TRUSTED_PROXYS\": \"${TRUSTED_PROXYS}\", + \"TRUSTED_PROXIES\": \"${TRUSTED_PROXIES}\", \"HA_MASTER\": \"${HA_MASTER}\", \"HA_MASTER_IP\": \"${HA_MASTER_IP}\", \"HA_NODE\": \"${HA_NODE}\", @@ -31,6 +37,8 @@ echo -n "\ \"TELEGRAM_BOT_TOKEN\": \"${TELEGRAM_BOT_TOKEN}\", \"TELEGRAM_CHAT_ID\": \"${TELEGRAM_CHAT_ID}\", \"WHATSAPP_API_URL\": \"${WHATSAPP_API_URL}\", - \"WHATSAPP_RECIPIENT\": \"${WHATSAPP_RECIPIENT}\" + \"WHATSAPP_RECIPIENT\": \"${WHATSAPP_RECIPIENT}\", + \"AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT\": \"${AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT}\", + \"LOG_LEVEL\": \"${LOG_LEVEL}\" } \ -" > ./src/data/variables.json +" > ./src/data/variables.json || exit 1 diff --git a/src/misc/createEnvFile.sh b/src/misc/createEnvFile.sh index 754eab5..0fbd15d 100755 --- a/src/misc/createEnvFile.sh +++ b/src/misc/createEnvFile.sh @@ -3,6 +3,9 @@ # Version VERSION="$(cat ./package.json | grep version | cut -d '"' -f 4)" +# Automatic Stack environment management +AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT="${AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT:-true}" + # Docker if grep -q '/docker' /proc/1/cgroup 2>/dev/null || [ -f /.dockerenv ]; then RUNNING_IN_DOCKER="true" @@ -10,11 +13,14 @@ else RUNNING_IN_DOCKER="false" fi +# Default log level +LOG_LEVEL="${LOG_LEVEL:-info}" + echo -n "\ { \"VERSION\": \"${VERSION}\", \"RUNNING_IN_DOCKER\": \"${RUNNING_IN_DOCKER}\", - \"TRUSTED_PROXYS\": \"${TRUSTED_PROXYS}\", + \"TRUSTED_PROXIES\": \"${TRUSTED_PROXIES}\", \"HA_MASTER\": \"${HA_MASTER}\", \"HA_MASTER_IP\": \"${HA_MASTER_IP}\", \"HA_NODE\": \"${HA_NODE}\", @@ -31,6 +37,8 @@ echo -n "\ \"TELEGRAM_BOT_TOKEN\": \"${TELEGRAM_BOT_TOKEN}\", \"TELEGRAM_CHAT_ID\": \"${TELEGRAM_CHAT_ID}\", \"WHATSAPP_API_URL\": \"${WHATSAPP_API_URL}\", - \"WHATSAPP_RECIPIENT\": \"${WHATSAPP_RECIPIENT}\" + \"WHATSAPP_RECIPIENT\": \"${WHATSAPP_RECIPIENT}\", + \"AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT\": \"${AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT}\", + \"LOG_LEVEL\": \"${LOG_LEVEL}\" } \ -" > /api/src/data/variables.json +" > /api/src/data/variables.json || exit 1 diff --git a/src/misc/dependencyGraphs/createDependencyGraph.sh b/src/misc/dependencyGraphs/createDependencyGraph.sh index 4e11819..5fe007a 100755 --- a/src/misc/dependencyGraphs/createDependencyGraph.sh +++ b/src/misc/dependencyGraphs/createDependencyGraph.sh @@ -11,7 +11,7 @@ spawn_worker(){ echo -e "\nRoute: $route \n${target_route}" - npx depcruise \ + test=true depcruise \ -c ./src/misc/dependencyGraphs/.dependency-cruiser.cjs \ -p cli-feedback \ -T mermaid \ diff --git a/src/misc/dependencyGraphs/mermaid-all.txt b/src/misc/dependencyGraphs/mermaid-all.txt index ad02a82..1cb2ebe 100644 --- a/src/misc/dependencyGraphs/mermaid-all.txt +++ b/src/misc/dependencyGraphs/mermaid-all.txt @@ -2,106 +2,112 @@ flowchart TB subgraph 0["src"] 1["server.ts"] -subgraph 2["controllers"] -3["highAvailability.ts"] -C["proxy.ts"] -D["scheduler.ts"] -F["fetchData.ts"] -Q["auth.ts"] -X["frontendConfiguration.ts"] -end -subgraph 4["config"] -5["variables.ts"] -B["initFiles.ts"] -E["db.ts"] -end -subgraph 6["data"] -7["variables.json"] -end -subgraph 8["typings"] -9["ha.ts"] -end -A["init.ts"] -subgraph G["middleware"] -H["authMiddleware.ts"] -K["checkLock.ts"] -L["rateLimiter.ts"] -end -subgraph I["handlers"] -J["response.ts"] -P["auth.ts"] -T["data.ts"] -W["frontend.ts"] -10["api.ts"] +2["init.ts"] +subgraph 3["config"] +4["initFiles.ts"] +7["variables.ts"] +B["db.ts"] +end +subgraph 5["controllers"] +6["proxy.ts"] +A["scheduler.ts"] +C["fetchData.ts"] +N["auth.ts"] +U["frontendConfiguration.ts"] +14["highAvailability.ts"] +end +subgraph 8["data"] +9["variables.json"] +end +subgraph D["middleware"] +E["authMiddleware.ts"] +H["checkLock.ts"] +I["rateLimiter.ts"] +end +subgraph F["handlers"] +G["response.ts"] +M["auth.ts"] +Q["data.ts"] +T["frontend.ts"] +X["api.ts"] +10["graph.ts"] 13["ha.ts"] -16["notification.ts"] -19["conf.ts"] +19["notification.ts"] +1C["conf.ts"] end -subgraph M["routes"] -subgraph N["auth"] -O["routes.ts"] +subgraph J["routes"] +subgraph K["auth"] +L["routes.ts"] end -subgraph R["data"] +subgraph O["data"] +P["routes.ts"] +end +subgraph R["frontendController"] S["routes.ts"] end -subgraph U["frontendController"] -V["routes.ts"] +subgraph V["getter"] +W["routes.ts"] end -subgraph Y["getter"] +subgraph Y["graphs"] Z["routes.ts"] end subgraph 11["highavailability"] 12["routes.ts"] end -subgraph 14["notifications"] -15["routes.ts"] -end -subgraph 17["setter"] +subgraph 17["notifications"] 18["routes.ts"] end +subgraph 1A["setter"] +1B["routes.ts"] +end +end +subgraph 15["typings"] +16["ha.ts"] end end -1-->3 -1-->A -3-->5 -3-->9 -5-->7 +1-->2 +2-->4 +2-->6 +2-->A +2-->E +2-->H +2-->I +2-->L +2-->P +2-->S +2-->W +2-->Z +2-->12 +2-->18 +2-->1B +6-->7 +7-->9 A-->B A-->C -A-->D -A-->H -A-->K -A-->L -A-->O -A-->S -A-->V -A-->Z -A-->12 -A-->15 -A-->18 -C-->5 -D-->E -D-->F -F-->E -H-->J -K-->J -O-->P +C-->B +E-->G +H-->G +L-->M +M-->N +M-->G P-->Q -P-->J +Q-->B +Q-->G S-->T -T-->E -T-->J -V-->W +T-->U +T-->G W-->X -W-->J +X-->A +X-->G Z-->10 -10-->D -10-->J +Z-->G 12-->13 -13-->3 -13-->J -15-->16 -16-->J +13-->14 +13-->G +14-->7 +14-->16 18-->19 -19-->D -19-->J +19-->G +1B-->1C +1C-->A +1C-->G diff --git a/src/misc/dependencyGraphs/mermaid-graph.txt b/src/misc/dependencyGraphs/mermaid-graph.txt new file mode 100644 index 0000000..3448453 --- /dev/null +++ b/src/misc/dependencyGraphs/mermaid-graph.txt @@ -0,0 +1,15 @@ +flowchart TB + +subgraph 0["src"] +subgraph 1["routes"] +subgraph 2["graphs"] +3["routes.ts"] +end +end +subgraph 4["handlers"] +5["graph.ts"] +6["response.ts"] +end +end +3-->5 +3-->6 diff --git a/src/misc/entrypoint.sh b/src/misc/entrypoint.sh index 60b8a0e..77b6236 100755 --- a/src/misc/entrypoint.sh +++ b/src/misc/entrypoint.sh @@ -3,6 +3,12 @@ VERSION="$(cat ./package.json | grep version | cut -d '"' -f 4)" +if [[ "$1" = "--dev" ]]; then + node_env="development" +elif [[ "$1" = "--prod" ]]; then + node_env="production" +fi + echo -e " \033[1;32mWelcome to\033[0m @@ -27,4 +33,4 @@ DockStat and DockStatAPI are 2 fully OpenSource projects, DockStatAPI is a simpl bash ./createEnvFile.sh -exec node src/server.js +NODE_ENV=${node_env} node src/server.js diff --git a/src/misc/minifyDist.sh b/src/misc/minifyDist.sh index 8a85b16..171ef09 100755 --- a/src/misc/minifyDist.sh +++ b/src/misc/minifyDist.sh @@ -4,7 +4,7 @@ dist="$(pwd)/dist" run_script() { npx uglifyjs --no-annotations --in-situ "$1" > /dev/null - echo "✔️ Minified : $(basename "$1")" + echo "✔️ Minified : $(basename "$1")" } if [ -d "$dist" ]; then diff --git a/src/routes/auth/routes.ts b/src/routes/auth/routes.ts index 47ff6f2..03549bf 100644 --- a/src/routes/auth/routes.ts +++ b/src/routes/auth/routes.ts @@ -3,54 +3,12 @@ import { createAuthenticationHandler } from "../../handlers/auth"; const router = Router(); -/** - * @swagger - * /auth/enable: - * post: - * summary: Enable authentication by setting a password - * tags: [Authentication] - * parameters: - * - name: password - * in: query - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Authentication enabled. - * 400: - * description: Password is required. - * 500: - * description: Error saving password. - */ router.post("/enable", async (req: Request, res: Response): Promise => { const password = req.query.password as string; const handler = createAuthenticationHandler(req, res); await handler.enable(password); }); -/** - * @swagger - * /auth/disable: - * post: - * summary: Disable authentication by providing the existing password - * tags: [Authentication] - * parameters: - * - name: password - * in: query - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Authentication disabled. - * 400: - * description: Password is required. - * 401: - * description: Invalid password. - * 500: - * description: Error disabling authentication. - */ router.post("/disable", async (req: Request, res: Response): Promise => { const password = req.query.password as string; const handler = createAuthenticationHandler(req, res); diff --git a/src/routes/data/routes.ts b/src/routes/data/routes.ts index 92a7f97..93c4610 100644 --- a/src/routes/data/routes.ts +++ b/src/routes/data/routes.ts @@ -2,150 +2,16 @@ import express, { Request, Response } from "express"; const router = express.Router(); import { createDatabaseHandler } from "../../handlers/data"; -/** - * @swagger - * /data/latest: - * get: - * summary: Retrieve the latest container statistics for a specific host - * tags: [Database queries] - * responses: - * 200: - * description: A JSON object containing the latest container statistics for the specified host. - * content: - * application/json: - * schema: - * type: object - * properties: - * Fin-2: - * type: array - * items: - * type: object - * properties: - * name: - * type: string - * description: The name of the container - * example: "Container A" - * id: - * type: string - * description: Unique identifier for the container - * example: "abcd1234" - * hostName: - * type: string - * description: Name of the host system running this container - * example: "Fin-2" - * state: - * type: string - * description: Current state of the container - * example: "running" - * cpu_usage: - * type: number - * description: CPU usage percentage for this container - * example: 30 - * mem_usage: - * type: number - * description: Memory usage in bytes - * example: 2097152 - * mem_limit: - * type: number - * description: Memory limit in bytes set for this container - * example: 8123764736 - * net_rx: - * type: number - * description: Total network received bytes since container start - * example: 151763111 - * net_tx: - * type: number - * description: Total network transmitted bytes since container start - * example: 7104386 - * current_net_rx: - * type: number - * description: Current received bytes in the recent period - * example: 1048576 - * current_net_tx: - * type: number - * description: Current transmitted bytes in the recent period - * example: 524288 - * networkMode: - * type: string - * description: Networking mode for the container - * example: "bridge" - */ router.get("/latest", (req: Request, res: Response) => { const DatabaseHandler = createDatabaseHandler(req, res); return DatabaseHandler.latest(); }); -/** - * @swagger - * /data/all: - * get: - * summary: Retrieve container statistics entries from the last 24 hours - * tags: [Database queries] - * responses: - * 200: - * description: A numbered array of 'info' JSON objects from the last 24 hours. - * content: - * application/json: - * schema: - * type: object - * properties: - * 0: - * type: object - * description: Statistics for the first entry within 24 hours. - * properties: - * name: - * type: string - * example: "Container A" - * id: - * type: string - * example: "abcd1234" - * cpu_usage: - * type: number - * example: 30 - * mem_usage: - * type: number - * example: 2048 - * 1: - * type: object - * description: Statistics for the second entry within 24 hours. - * properties: - * name: - * type: string - * example: "Container B" - * id: - * type: string - * example: "efgh5678" - * cpu_usage: - * type: number - * example: 45 - * mem_usage: - * type: number - * example: 3072 - */ router.get("/all", (req: Request, res: Response) => { const DatabaseHandler = createDatabaseHandler(req, res); return DatabaseHandler.all(); }); -/** - * @swagger - * /data/clear: - * delete: - * summary: Clear all container statistics entries from the database - * tags: [Database queries] - * responses: - * 200: - * description: A message indicating whether the database was cleared successfully. - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * description: Success message upon database clearance - * example: "Database cleared successfully." - */ router.delete("/clear", (req: Request, res: Response) => { const DatabaseHandler = createDatabaseHandler(req, res); return DatabaseHandler.clear(); diff --git a/src/routes/frontendController/routes.ts b/src/routes/frontendController/routes.ts index 39500c5..723afa4 100644 --- a/src/routes/frontendController/routes.ts +++ b/src/routes/frontendController/routes.ts @@ -2,259 +2,30 @@ import express from "express"; const router = express.Router(); import { createFrontendHandler } from "../../handlers/frontend"; -/** - * @swagger - * /frontend/show/{containerName}: - * post: - * summary: Unhide a container - * tags: [Frontend Configuration] - * parameters: - * - in: path - * name: containerName - * schema: - * type: string - * required: true - * description: The name of the container to unhide - * responses: - * 200: - * description: Container unhidden successfully. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: Indicates if the operation was successful - * message: - * type: string - * description: Success message - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: Indicates if the operation was successful - * error: - * type: string - * description: Error message - */ router.post("/show/:containerName", async (req, res) => { const FrontendHandler = createFrontendHandler(req, res); const containerName = req.params.containerName; return FrontendHandler.show(containerName); }); -/** - * @swagger - * /frontend/tag/{containerName}/{tag}: - * post: - * summary: Add a tag to a container - * tags: [Frontend Configuration] - * parameters: - * - in: path - * name: containerName - * schema: - * type: string - * required: true - * description: The name of the container to add tag to - * - in: path - * name: tag - * schema: - * type: string - * required: true - * description: The tag to add - * responses: - * 200: - * description: Tag added successfully. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: Indicates if the operation was successful - * message: - * type: string - * description: Success message - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: Indicates if the operation was successful - * error: - * type: string - * description: Error message - */ router.post("/tag/:containerName/:tag", async (req, res) => { const { containerName, tag } = req.params; const FrontendHandler = createFrontendHandler(req, res); return FrontendHandler.addTag(containerName, tag); }); -/** - * @swagger - * /frontend/pin/{containerName}: - * post: - * summary: Pin a container - * tags: [Frontend Configuration] - * parameters: - * - in: path - * name: containerName - * schema: - * type: string - * required: true - * description: The name of the container to pin - * responses: - * 200: - * description: Container pinned successfully. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: Indicates if the operation was successful - * message: - * type: string - * description: Success message - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: Indicates if the operation was successful - * error: - * type: string - * description: Error message - */ router.post("/pin/:containerName", async (req, res) => { const { containerName } = req.params; const FrontendHandler = createFrontendHandler(req, res); return FrontendHandler.pin(containerName); }); -/** - * @swagger - * /frontend/add-link/{containerName}/{link}: - * post: - * summary: Add a link to a container - * tags: [Frontend Configuration] - * parameters: - * - in: path - * name: containerName - * schema: - * type: string - * required: true - * description: The name of the container to add link to - * - in: path - * name: link - * schema: - * type: string - * required: true - * description: The link to add - * responses: - * 200: - * description: Link added successfully. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: Indicates if the operation was successful - * message: - * type: string - * description: Success message - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: Indicates if the operation was successful - * error: - * type: string - * description: Error message - */ router.post("/add-link/:containerName/:link", async (req, res) => { const { containerName, link } = req.params; const FrontendHandler = createFrontendHandler(req, res); return FrontendHandler.addLink(containerName, link); }); -/** - * @swagger - * /frontend/add-icon/{containerName}/{icon}/{useCustomIcon}: - * post: - * summary: Add an Icon to a container - * tags: [Frontend Configuration] - * parameters: - * - in: path - * name: containerName - * schema: - * type: string - * required: true - * description: The name of the container to add link to - * - in: path - * name: icon - * schema: - * type: string - * required: true - * description: The Icon to add - * - in: path - * name: useCustomIcon - * shema: - * type: boolean - * required: false - * description: If this icon is a custom icon or nor - * responses: - * 200: - * description: Icon added successfully. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: Indicates if the operation was successful - * message: - * type: string - * description: Success message - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: Indicates if the operation was successful - * error: - * type: string - * description: Error message - */ router.post( "/add-icon/:containerName/:icon/:useCustomIcon", async (req, res) => { @@ -272,242 +43,30 @@ router.post( |____/|_____|_____|_____| |_| |_____| */ -/** - * @swagger - * /frontend/hide/{containerName}: - * delete: - * summary: Hide a container - * tags: [Frontend Configuration] - * parameters: - * - in: path - * name: containerName - * schema: - * type: string - * required: true - * description: The name of the container to hide - * responses: - * 200: - * description: Container hidden successfully. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: Indicates if the operation was successful - * message: - * type: string - * description: Success message - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: Indicates if the operation was successful - * error: - * type: string - * description: Error message - */ -// Hide a container router.delete("/hide/:containerName", async (req, res) => { const { containerName } = req.params; const FrontendHandler = createFrontendHandler(req, res); return FrontendHandler.hide(containerName); }); -/** - * @swagger - * /frontend/remove-tag/{containerName}/{tag}: - * delete: - * summary: Remove a tag from a container - * tags: [Frontend Configuration] - * parameters: - * - in: path - * name: containerName - * schema: - * type: string - * required: true - * description: The name of the container to remove tag from - * - in: path - * name: tag - * schema: - * type: string - * required: true - * description: The tag to remove - * responses: - * 200: - * description: Tag removed successfully. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: Indicates if the operation was successful - * message: - * type: string - * description: Success message - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: Indicates if the operation was successful - * error: - * type: string - * description: Error message - */ router.delete("/remove-tag/:containerName/:tag", async (req, res) => { const { containerName, tag } = req.params; const FrontendHandler = createFrontendHandler(req, res); return FrontendHandler.removeTag(containerName, tag); }); -/** - * @swagger - * /frontend/unpin/{containerName}: - * delete: - * summary: Unpin a container - * tags: [Frontend Configuration] - * parameters: - * - in: path - * name: containerName - * schema: - * type: string - * required: true - * description: The name of the container to unpin - * responses: - * 200: - * description: Container unpinned successfully. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: Indicates if the operation was successful - * message: - * type: string - * description: Success message - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: Indicates if the operation was successful - * error: - * type: string - * description: Error message - */ router.delete("/unpin/:containerName", async (req, res) => { const { containerName } = req.params; const FrontendHandler = createFrontendHandler(req, res); return FrontendHandler.unPin(containerName); }); -/** - * @swagger - * /frontend/remove-link/{containerName}: - * delete: - * summary: Remove a link from a container - * tags: [Frontend Configuration] - * parameters: - * - in: path - * name: containerName - * schema: - * type: string - * required: true - * description: The name of the container to remove link from - * responses: - * 200: - * description: Link removed successfully. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: Indicates if the operation was successful - * message: - * type: string - * description: Success message - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: Indicates if the operation was successful - * error: - * type: string - * description: Error message - */ router.delete("/remove-link/:containerName", async (req, res) => { const { containerName } = req.params; const FrontendHandler = createFrontendHandler(req, res); return FrontendHandler.removeLink(containerName); }); -/** - * @swagger - * /frontend/remove-icon/{containerName}: - * delete: - * summary: Remove an icon from a container - * tags: [Frontend Configuration] - * parameters: - * - in: path - * name: containerName - * schema: - * type: string - * required: true - * description: The name of the container to remove the icon from - * responses: - * 200: - * description: Icon removed successfully. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: Indicates if the operation was successful - * message: - * type: string - * description: Success message - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: Indicates if the operation was successful - * error: - * type: string - * description: Error message - */ router.delete("/remove-icon/:containerName", async (req, res) => { const { containerName } = req.params; const FrontendHandler = createFrontendHandler(req, res); diff --git a/src/routes/getter/routes.ts b/src/routes/getter/routes.ts index 0912d48..d08ae51 100644 --- a/src/routes/getter/routes.ts +++ b/src/routes/getter/routes.ts @@ -2,315 +2,42 @@ import { Router, Request, Response } from "express"; import { createApiHandler } from "../../handlers/api"; const router = Router(); -/** - * @swagger - * /api/hosts: - * get: - * summary: Retrieve a list of all available Docker hosts - * tags: [Hosts] - * responses: - * 200: - * description: A JSON object containing an array of host names. - * content: - * application/json: - * schema: - * type: object - * properties: - * hosts: - * type: array - * items: - * type: string - * example: ["local", "remote1"] - */ router.get("/hosts", (req: Request, res: Response) => { const ApiHandler = createApiHandler(req, res); return ApiHandler.hosts(); }); -/** - * @swagger - * /api/system: - * get: - * summary: Retrieve system configuration details - * tags: [Misc] - * responses: - * 200: - * description: A JSON object containing the system configuration details. - * content: - * application/json: - * schema: - * type: object - * description: The parsed configuration details. - * 500: - * description: An error occurred while fetching the system configuration. - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * description: Error message detailing the issue encountered. - */ router.get("/system", (req: Request, res: Response) => { const ApiHandler = createApiHandler(req, res); return ApiHandler.system(); }); -/** - * @swagger - * /api/host/{hostName}/stats: - * get: - * summary: Retrieve statistics for a specified Docker host - * tags: [Hosts] - * parameters: - * - name: hostName - * in: path - * required: true - * description: The name of the host for which to fetch statistics. - * schema: - * type: string - * responses: - * 200: - * description: A JSON object containing relevant statistics for the specified host. - * content: - * application/json: - * schema: - * type: object - * properties: - * hostName: - * type: string - * description: The name of the Docker host. - * info: - * type: object - * description: Information about the Docker host (e.g., storage, running containers). - * version: - * type: object - * description: Version details of the Docker installation on the host. - * 500: - * description: An error occurred while fetching host statistics. - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * description: Error message detailing the issue encountered. - */ router.get("/host/:hostName/stats", async (req: Request, res: Response) => { const { hostName } = req.params; const ApiHandler = createApiHandler(req, res); return ApiHandler.hostStats(hostName); }); -/** - * @swagger - * /api/containers: - * get: - * summary: Retrieve all Docker containers across all configured hosts - * tags: [Containers] - * responses: - * 200: - * description: A JSON object containing container data for all hosts. - * content: - * application/json: - * schema: - * type: object - * additionalProperties: - * type: object - * properties: - * name: - * type: string - * description: Name of the container. - * id: - * type: string - * description: Unique identifier for the container. - * hostName: - * type: string - * description: The host on which the container is running. - * state: - * type: string - * description: Current state of the container (e.g., running, exited). - * cpu_usage: - * type: number - * format: double - * description: CPU usage in nanoseconds. - * mem_usage: - * type: number - * description: Memory usage in bytes. - * mem_limit: - * type: number - * description: Memory limit in bytes. - * net_rx: - * type: number - * description: Total received bytes over the network. - * net_tx: - * type: number - * description: Total transmitted bytes over the network. - * current_net_rx: - * type: number - * description: Current received bytes over the network. - * current_net_tx: - * type: number - * description: Current transmitted bytes over the network. - * networkMode: - * type: string - * description: Network mode configured for the container. - * link: - * type: string - * description: Optional link to additional information. - * icon: - * type: string - * description: Optional icon representing the container. - * tags: - * type: string - * description: Optional tags associated with the container. - * 500: - * description: An error occurred while fetching container data. - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * description: Error message detailing the issue encountered. - */ router.get("/containers", async (req: Request, res: Response) => { const ApiHandler = createApiHandler(req, res); return ApiHandler.containers(); }); -/** - * @swagger - * /api/config: - * get: - * summary: Retrieve Docker configuration - * tags: [Configuration] - * responses: - * 200: - * description: A JSON object containing the Docker configuration. - * content: - * application/json: - * schema: - * type: object - * additionalProperties: true - * 500: - * description: An error occurred while loading the Docker configuration. - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * description: Error message detailing the issue encountered. - */ router.get("/config", async (req: Request, res: Response) => { const ApiHandler = createApiHandler(req, res); return ApiHandler.config(); }); -/** - * @swagger - * /api/current-schedule: - * get: - * summary: Get the current fetch schedule in seconds - * tags: [Configuration] - * responses: - * 200: - * description: Current fetch schedule retrieved successfully. - * content: - * application/json: - * schema: - * type: object - * properties: - * interval: - * type: integer - * description: Current fetch interval in seconds. - */ router.get("/current-schedule", (req: Request, res: Response) => { const ApiHandler = createApiHandler(req, res); return ApiHandler.currentSchedule(); }); -/** - * @swagger - * /api/status: - * get: - * summary: Check the DockStatAPI and docker socket status of each host - * tags: [Misc] - * description: Returns the status of the backend and online components, indicating which nodes are reachable or offline. - * responses: - * 200: - * description: Server and backend status - * content: - * application/json: - * schema: - * type: object - * properties: - * backendReachable: - * type: boolean - * example: true - * online: - * type: object - * properties: - * Host-1: - * type: boolean - * example: true - * Host-2: - * type: boolean - * example: false - */ - router.get("/status", async (req: Request, res: Response) => { const ApiHandler = createApiHandler(req, res); return ApiHandler.status(); }); -/** - * @swagger - * /api/frontend-config: - * get: - * summary: Get Frontend Configuration - * tags: [Configuration] - * description: Retrieves the frontend configuration data. - * responses: - * 200: - * description: Success - * content: - * application/json: - * schema: - * type: array - * items: - * type: object - * properties: - * name: - * type: string - * description: Container Name - * hidden: - * type: boolean - * description: Whether the container is hidden - * tags: - * type: array - * items: - * type: string - * description: Tags associated with the container - * pinned: - * type: boolean - * description: Whether the container is pinned - * 500: - * description: Internal Server Error - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * description: Error message - */ router.get("/frontend-config", (req: Request, res: Response) => { const ApiHandler = createApiHandler(req, res); return ApiHandler.frontendConfig(); diff --git a/src/routes/graphs/routes.ts b/src/routes/graphs/routes.ts new file mode 100644 index 0000000..db53205 --- /dev/null +++ b/src/routes/graphs/routes.ts @@ -0,0 +1,31 @@ +import { Request, Response, Router } from "express"; +import { createResponseHandler } from "../../handlers/response"; +import path from "path"; +const router = Router(); + +router.get("/", async (req: Request, res: Response) => { + const ResponseHandler = createResponseHandler(res); + try { + const graphPath = path.join( + __dirname, + "/../../.." + "/src/data/graph.html", + ); + return res.contentType("html").status(200).sendFile(graphPath); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } +}); + +router.get("/image", async (req: Request, res: Response) => { + const ResponseHandler = createResponseHandler(res); + try { + const graphPath = path.join(__dirname, "/../../.." + "/src/data/graph.png"); + return res.contentType("image/png").status(200).sendFile(graphPath); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } +}); + +export default router; diff --git a/src/routes/highavailability/routes.ts b/src/routes/highavailability/routes.ts index 86057bc..d4adc46 100644 --- a/src/routes/highavailability/routes.ts +++ b/src/routes/highavailability/routes.ts @@ -3,31 +3,11 @@ import { SyncRequestBody } from "../../typings/syncRequestBody"; import { createHaHandler } from "../../handlers/ha"; const router = Router(); -/** - * @swagger - * /ha/config: - * get: - * summary: Retrieve the High Availability Config - * tags: [High Availability] - * responses: - * 200: - * description: A JSON object containing the config. - */ router.get("/config", async (req: Request, res: Response) => { const HaHandler = createHaHandler(req, res); return HaHandler.config(); }); -/** - * @swagger - * /ha/sync: - * post: - * summary: Synchronize configuration files from master node. - * tags: [High Availability] - * responses: - * 200: - * description: Files synchronized successfully. - */ router.post( "/sync", async ( @@ -39,16 +19,6 @@ router.post( }, ); -/** - * @swagger - * /ha/prepare-sync: - * get: - * summary: Prepare files for synchronization. - * tags: [High Availability] - * responses: - * 200: - * description: A JSON object containing files to sync. - */ router.get("/prepare-sync", async (req: Request, res: Response) => { const HaHandler = createHaHandler(req, res); return HaHandler.prepare(); diff --git a/src/routes/notifications/routes.ts b/src/routes/notifications/routes.ts index 4544b8c..13b754b 100644 --- a/src/routes/notifications/routes.ts +++ b/src/routes/notifications/routes.ts @@ -2,125 +2,16 @@ import { Request, Response, Router } from "express"; import { createNotificationHandler } from "../../handlers/notification"; const router = Router(); -/** - * @swagger - * /notification-service/get-template: - * get: - * summary: Retrieve the notification template - * tags: [Notification Service] - * responses: - * 200: - * description: Template data retrieved successfully. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: Indicates if the operation was successful - * data: - * type: object - * description: The template data in JSON format - * 500: - * description: Internal server error. - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * description: Error message - */ router.get("/get-template", (req: Request, res: Response) => { const NotificationHandler = createNotificationHandler(req, res); return NotificationHandler.getTemplate(); }); -/** - * @swagger - * /notification-service/set-template: - * post: - * summary: Update the notification template - * tags: [Notification Service] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * description: New template data to save - * responses: - * 200: - * description: Template updated successfully. - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * description: Success message - * 500: - * description: Internal server error. - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * description: Error message - */ router.post("/set-template", (req: Request, res: Response): void => { const NotificationHandler = createNotificationHandler(req, res); return NotificationHandler.setTemplate(req); }); -/** - * @swagger - * /notification-service/test/{type}/{containerId}: - * post: - * summary: Send a test notification for a specific container - * tags: [Notification Service] - * parameters: - * - in: path - * name: type - * schema: - * type: string - * required: true - * description: Type of notification to test - * - in: path - * name: containerId - * schema: - * type: string - * required: true - * description: The ID of the container for the notification test - * responses: - * 200: - * description: Test notification sent successfully. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * message: - * type: string - * 500: - * description: Internal server error. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * message: - * type: string - */ router.post("/test/:type/:containerId", async (req: Request, res: Response) => { const NotificationHandler = createNotificationHandler(req, res); NotificationHandler.test(req); diff --git a/src/routes/setter/routes.ts b/src/routes/setter/routes.ts index 75ef747..1615029 100644 --- a/src/routes/setter/routes.ts +++ b/src/routes/setter/routes.ts @@ -1,83 +1,20 @@ import express, { Router, Request, Response } from "express"; -import { createConfHandler } from "../../handlers/conf"; const router: Router = express.Router(); +import { createConfHandler } from "../../handlers/conf"; -/** - * @swagger - * /conf/addHost: - * put: - * summary: Add a new host to the Docker configuration - * tags: [Configuration] - * parameters: - * - name: name - * in: query - * required: true - * description: The name of the new host. - * - name: url - * in: query - * required: true - * description: The URL of the new host. - * - name: port - * in: query - * required: true - * description: The port of the new host. - * responses: - * 200: - * description: Host added successfully. - * 400: - * description: Bad request, invalid input. - * 500: - * description: An error occurred while adding the host. - */ router.put("/addHost", async (req: Request, res: Response): Promise => { const ConfHandler = createConfHandler(req, res); return ConfHandler.addHost(req); }); -/** - * @swagger - * /conf/scheduler: - * put: - * summary: Set fetch interval for data fetching - * tags: [Configuration] - * parameters: - * - name: interval - * in: query - * required: true - * description: The new interval for fetching data, e.g., "6h 20m", "300s". - * responses: - * 200: - * description: Fetch interval set successfully. - * 400: - * description: Invalid interval format or out of range. - */ -router.put("/scheduler", (req: Request, res: Response) => { +router.delete("/removeHost", (req: Request, res: Response): void => { const ConfHandler = createConfHandler(req, res); - return ConfHandler.scheduler(req); + return ConfHandler.removeHost(req); }); -/** - * @swagger - * /conf/removeHost: - * delete: - * summary: Remove a host from the Docker configuration - * tags: [Configuration] - * parameters: - * - name: hostName - * in: query - * required: true - * description: The name of the host to remove. - * responses: - * 200: - * description: Host removed successfully. - * 404: - * description: Host not found. - * 500: - * description: An error occurred while removing the host. - */ -router.delete("/removeHost", (req: Request, res: Response): void => { +router.put("/scheduler", (req: Request, res: Response) => { const ConfHandler = createConfHandler(req, res); - return ConfHandler.addHost(req); + return ConfHandler.scheduler(req); }); export default router; diff --git a/src/routes/stack/routes.ts b/src/routes/stack/routes.ts new file mode 100644 index 0000000..8f9b9ae --- /dev/null +++ b/src/routes/stack/routes.ts @@ -0,0 +1,35 @@ +import express, { Router, Request, Response } from "express"; +const router: Router = express.Router(); +import { createStackHandler } from "../../handlers/stack"; + +router.post("/create/:name", async (req: Request, res: Response) => { + const StackHandler = createStackHandler(req, res); + return StackHandler.createStack(req, res); +}); + +router.post("/start/:name", async (req: Request, res: Response) => { + const StackHandler = createStackHandler(req, res); + return StackHandler.start(req, res); +}); + +router.post("/stop/:name", async (req: Request, res: Response) => { + const StackHandler = createStackHandler(req, res); + return StackHandler.stop(req, res); +}); + +router.get("/get/:name", async (req: Request, res: Response) => { + const StackHandler = createStackHandler(req, res); + return await StackHandler.stackCompose(req, res); +}); + +router.post("/set-env/:name", async (req: Request, res: Response) => { + const StackHandler = createStackHandler(req, res); + return await StackHandler.setStackEnv(req, res); +}); + +router.get("/get-env/:name", async (req: Request, res: Response) => { + const StackHandler = createStackHandler(req, res); + return await StackHandler.getStackEnv(req, res); +}); + +export default router; diff --git a/src/sample-variable.json b/src/sample-variable.json index 06153af..f507796 100644 --- a/src/sample-variable.json +++ b/src/sample-variable.json @@ -1,7 +1,7 @@ { "VERSION": "", "RUNNING_IN_DOCKER": "", - "TRUSTED_PROXYS": "", + "TRUSTED_PROXIES": "", "HA_MASTER": "", "HA_MASTER_IP": "", "HA_NODE": "", @@ -18,5 +18,7 @@ "TELEGRAM_BOT_TOKEN": "", "TELEGRAM_CHAT_ID": "", "WHATSAPP_API_URL": "", - "WHATSAPP_RECIPIENT": "" + "WHATSAPP_RECIPIENT": "", + "AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT": "true", + "LOG_LEVEL": "info" } diff --git a/src/server.ts b/src/server.ts index 97e5337..edcb2ec 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,14 +1,18 @@ import express from "express"; import initializeApp from "./init"; -import { startMasterNode } from "./controllers/highAvailability"; import writeUserConf from "./config/hostsystem"; +import { startServer } from "./utils/startServer"; +import http from "http"; +const port: number = parseInt(process.env.PORT || "9876"); const app = express(); -const PORT: number = 9876; +const server = http.createServer(app); -writeUserConf(); -initializeApp(app); +initializeApp(app, server); -app.listen(PORT, () => { - startMasterNode(); -}); +if (process.env.NODE_ENV !== "testing") { + writeUserConf(port); + startServer(app, server, port); +} + +export default app; \ No newline at end of file diff --git a/src/typings/dockerCompose.ts b/src/typings/dockerCompose.ts new file mode 100644 index 0000000..e30f7e0 --- /dev/null +++ b/src/typings/dockerCompose.ts @@ -0,0 +1,92 @@ +export interface DockerComposeFile { + services: Record; + networks?: Record; + volumes?: Record; +} + +export interface ServiceDefinition { + image?: string; + build?: BuildDefinition; + container_name?: string; + command?: string | string[]; + environment?: Record; + ports?: string[] | PortMapping[]; + volumes?: string[]; + networks?: string[]; + restart?: string; + depends_on?: string[]; + deploy?: DeployDefinition; + env_file?: string[]; +} + +export interface BuildDefinition { + context: string; + dockerfile?: string; + args?: Record; + cache_from?: string[]; + labels?: Record; + target?: string; +} + +export interface PortMapping { + target: number; + published: number; + protocol?: "tcp" | "udp"; + mode?: "host" | "ingress"; +} + +export interface DeployDefinition { + replicas?: number; + resources?: ResourcesDefinition; + restart_policy?: RestartPolicyDefinition; + labels?: Record; + update_config?: UpdateConfigDefinition; +} + +export interface ResourcesDefinition { + limits?: ResourceLimits; + reservations?: ResourceReservations; +} + +export interface ResourceLimits { + cpus?: string; + memory?: string; +} + +export interface ResourceReservations { + cpus?: string; + memory?: string; +} + +export interface RestartPolicyDefinition { + condition?: "none" | "on-failure" | "any"; + delay?: string; + max_attempts?: number; + window?: string; +} + +export interface UpdateConfigDefinition { + parallelism?: number; + delay?: string; + failure_action?: "continue" | "pause"; + monitor?: string; + max_failure_ratio?: number; + order?: "start-first" | "stop-first"; +} + +export interface NetworkDefinition { + driver?: string; + driver_opts?: Record; + attachable?: boolean; + external?: boolean; + internal?: boolean; + labels?: Record; +} + +export interface VolumeDefinition { + driver?: string; + driver_opts?: Record; + external?: boolean; + labels?: Record; + name?: string; +} diff --git a/src/typings/dockerStackEnv.ts b/src/typings/dockerStackEnv.ts new file mode 100644 index 0000000..c784b85 --- /dev/null +++ b/src/typings/dockerStackEnv.ts @@ -0,0 +1,10 @@ +interface dockerStackProperty { + name: string; + value: string; +} + +interface dockerStackEnv { + environment: dockerStackProperty[]; +} + +export { dockerStackEnv, dockerStackProperty }; diff --git a/src/typings/ha.ts b/src/typings/ha.ts index a722fff..f0352fc 100644 --- a/src/typings/ha.ts +++ b/src/typings/ha.ts @@ -6,7 +6,7 @@ interface HighAvailabilityConfig { interface Node { ip: string; - id: number; + port: number; } interface HaNodeConfig { diff --git a/src/typings/stackConfig.ts b/src/typings/stackConfig.ts new file mode 100644 index 0000000..45c7255 --- /dev/null +++ b/src/typings/stackConfig.ts @@ -0,0 +1,5 @@ +interface stackConfig { + stacks: string[]; +} + +export { stackConfig }; diff --git a/src/utils/assets/api-icon.svg b/src/utils/assets/api-icon.svg new file mode 100644 index 0000000..5a4fdb7 --- /dev/null +++ b/src/utils/assets/api-icon.svg @@ -0,0 +1 @@ +\ diff --git a/src/utils/assets/container-icon.svg b/src/utils/assets/container-icon.svg new file mode 100644 index 0000000..15ed98c --- /dev/null +++ b/src/utils/assets/container-icon.svg @@ -0,0 +1 @@ +\ diff --git a/src/utils/assets/server-icon.svg b/src/utils/assets/server-icon.svg new file mode 100644 index 0000000..31c92d4 --- /dev/null +++ b/src/utils/assets/server-icon.svg @@ -0,0 +1 @@ +\ diff --git a/src/utils/atomicWrite.ts b/src/utils/atomicWrite.ts index 51f3375..d279475 100644 --- a/src/utils/atomicWrite.ts +++ b/src/utils/atomicWrite.ts @@ -4,7 +4,7 @@ import { AtomicWriteOptions } from "../typings/atomicWrite"; export function atomicWrite( targetPath: string, - data: string | Buffer | Record, + data: object | string | Buffer | Record, options: AtomicWriteOptions = {}, ): void { const { mode = 0o600, exclusive = false } = options; diff --git a/src/utils/connectionChecker.ts b/src/utils/connectionChecker.ts index 85b00dd..5a45505 100644 --- a/src/utils/connectionChecker.ts +++ b/src/utils/connectionChecker.ts @@ -59,8 +59,8 @@ async function checkReachability(): Promise { const hosts: target[] = parsedData.hosts; return await checkHostStatus(hosts); } catch (error: unknown) { - logger.error(`Error reading file: ${error as Error}`); - return undefined; + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } } diff --git a/src/utils/containerService.ts b/src/utils/containerService.ts index f9277c1..86dc2d3 100644 --- a/src/utils/containerService.ts +++ b/src/utils/containerService.ts @@ -1,16 +1,18 @@ import logger from "./logger"; -import { ContainerInfo, ContainerStats, ContainerInspectInfo } from "dockerode"; -import getDockerClient from "./dockerClient"; +import { ContainerInfo, } from "dockerode"; +import { getDockerClient } from "./dockerClient"; import fs from "fs"; import { atomicWrite } from "./atomicWrite"; const configPath = "./src/data/dockerConfig.json"; import { AllContainerData, HostConfig } from "../typings/dockerConfig"; +import { generateGraphFiles } from "../handlers/graph"; +import { WebSocket } from "ws"; -function loadConfig() { +export function loadConfig() { try { if (!fs.existsSync(configPath)) { logger.warn( - `Config file not found. Creating an empty file at ${configPath}`, + `Config file not found. Creating an empty file at ${configPath}` ); atomicWrite(configPath, JSON.stringify({ hosts: [] }, null, 2)); } @@ -19,96 +21,147 @@ function loadConfig() { logger.debug("Loaded " + configPath); return JSON.parse(configData); } catch (error: unknown) { - logger.error(`Failed to load config: ${(error as Error).message}`); - return null; + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + return { hosts: [] }; } } -async function fetchAllContainers(): Promise { +export async function fetchContainersForHost(hostName: string) { const config = loadConfig(); - if (!config || !config.hosts) { - logger.error("Invalid or missing host configuration."); - return {}; + const hostConfig = config.hosts.find((h: HostConfig) => h.name === hostName); + + if (!hostConfig) { + throw new Error(`Host ${hostName} not found in configuration`); } - const allContainerData: AllContainerData = {}; + try { + const docker = getDockerClient(hostName); + const containers: ContainerInfo[] = await docker.listContainers({ all: true }); - for (const hostConfig of config.hosts as HostConfig[]) { - const hostName = hostConfig.name; - try { - const docker = getDockerClient(hostName); - logger.debug(`Now processing: ${hostName}`); - const containers: ContainerInfo[] = await docker.listContainers({ - all: true, - }); - - allContainerData[hostName] = await Promise.all( - containers.map(async (container) => { - try { - const containerInstance = docker.getContainer(container.Id); - const containerInfo: ContainerInspectInfo = - await containerInstance.inspect(); - const containerStats: ContainerStats = - await containerInstance.stats({ stream: false }); - - const cpuDelta = - containerStats.cpu_stats.cpu_usage.total_usage - - containerStats.precpu_stats.cpu_usage.total_usage; - const systemCpuDelta = - containerStats.cpu_stats.system_cpu_usage - - containerStats.precpu_stats.system_cpu_usage; - const cpuUsage = - systemCpuDelta > 0 - ? (cpuDelta / systemCpuDelta) * - containerStats.cpu_stats.online_cpus - : 0; - - return { - name: container.Names[0].replace("/", ""), - id: container.Id, - hostName, - state: container.State, - cpu_usage: cpuUsage * 1000000000, - mem_usage: containerStats.memory_stats.usage, - mem_limit: containerStats.memory_stats.limit, - net_rx: containerStats.networks?.eth0?.rx_bytes || 0, - net_tx: containerStats.networks?.eth0?.tx_bytes || 0, - current_net_rx: containerStats.networks?.eth0?.rx_bytes || 0, - current_net_tx: containerStats.networks?.eth0?.tx_bytes || 0, - networkMode: containerInfo.HostConfig.NetworkMode || "unknown", - }; - } catch (containerError: unknown) { - logger.error( - `Error fetching details for container ID: ${container.Id} on host: ${hostName} - ${(containerError as Error).message}`, - ); - return { - name: container.Names[0].replace("/", ""), - id: container.Id, - hostName, - state: container.State, - cpu_usage: 0, - mem_usage: 0, - mem_limit: 0, - net_rx: 0, - net_tx: 0, - current_net_rx: 0, - current_net_tx: 0, - networkMode: "unknown", - }; - } - }), - ); - } catch (error: unknown) { - logger.error( - `Error fetching containers for host: ${hostName} - ${(error as Error).message}. Stack: ${(error as Error).stack}`, - ); - allContainerData[hostName] = { - error: `Error fetching containers: ${(error as Error).message}`, - }; - } + return await Promise.all( + containers.map(async (container) => { + try { + const containerInstance = docker.getContainer(container.Id); + const [containerInfo, containerStats] = await Promise.all([ + containerInstance.inspect(), + containerInstance.stats({ stream: false }), + ]); + + const cpuDelta = + containerStats.cpu_stats.cpu_usage.total_usage - + containerStats.precpu_stats.cpu_usage.total_usage; + const systemCpuDelta = + containerStats.cpu_stats.system_cpu_usage - + containerStats.precpu_stats.system_cpu_usage; + const cpuUsage = + systemCpuDelta > 0 + ? (cpuDelta / systemCpuDelta) * containerStats.cpu_stats.online_cpus + : 0; + + return { + name: container.Names[0].replace("/", ""), + id: container.Id, + hostName, + state: container.State, + cpu_usage: cpuUsage, + mem_usage: containerStats.memory_stats.usage, + mem_limit: containerStats.memory_stats.limit, + net_rx: containerStats.networks?.eth0?.rx_bytes || 0, + net_tx: containerStats.networks?.eth0?.tx_bytes || 0, + current_net_rx: containerStats.networks?.eth0?.rx_bytes || 0, + current_net_tx: containerStats.networks?.eth0?.tx_bytes || 0, + networkMode: containerInfo.HostConfig.NetworkMode || "unknown", + }; + } catch (error) { + logger.error(`Error processing container ${container.Id}: ${error}`); + return { + name: container.Names[0].replace("/", ""), + id: container.Id, + hostName, + state: container.State, + cpu_usage: 0, + mem_usage: 0, + mem_limit: 0, + net_rx: 0, + net_tx: 0, + current_net_rx: 0, + current_net_tx: 0, + networkMode: "unknown", + }; + } + }) + ); + } catch (error) { + logger.error(`Error fetching containers for ${hostName}: ${error}`); + throw error; } +} + +export async function fetchAllContainers(): Promise { + const config = loadConfig(); + const allContainerData: AllContainerData = {}; + await Promise.all( + config.hosts.map(async (hostConfig: HostConfig) => { + try { + allContainerData[hostConfig.name] = await fetchContainersForHost(hostConfig.name); + } catch (error) { + allContainerData[hostConfig.name] = { + error: `Error fetching containers: ${error instanceof Error ? error.message : String(error)}` + }; + } + }) + ); + + generateGraphFiles(allContainerData); return allContainerData; } -export default fetchAllContainers; +export async function streamContainerData(ws: WebSocket, hostName: string) { + try { + const containers = await fetchContainersForHost(hostName); + ws.send(JSON.stringify({ type: "containers", data: containers })); + + const docker = getDockerClient(hostName); + const eventStream = await docker.getEvents(); + + // eslint-disable-next-line + if (!(eventStream instanceof require('stream').Readable)) { + throw new Error('Failed to get valid event stream'); + } + + const handleData = (chunk: Buffer) => { + ws.send(JSON.stringify({ type: "container-event", data: chunk.toString() })); + }; + + const handleError = (err: Error) => { + logger.error(`Event stream error for ${hostName}: ${err.message}`); + ws.close(); + }; + + eventStream + .on('data', handleData) + .on('error', handleError); + + const closeHandler = () => { + eventStream + .removeListener('data', handleData) + .removeListener('error', handleError) + .removeListener('closed', handleError); + logger.info(`Closed event stream for ${hostName}`); + }; + + ws.on('close', closeHandler); + ws.on('error', closeHandler); + + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + logger.error("Container data error:", message); + ws.send(JSON.stringify({ + error: "Failed to fetch container data", + details: message + })); + ws.close(); + } +} \ No newline at end of file diff --git a/src/utils/dockerClient.ts b/src/utils/dockerClient.ts index 8f2718b..469c409 100644 --- a/src/utils/dockerClient.ts +++ b/src/utils/dockerClient.ts @@ -1,7 +1,6 @@ import Docker from "dockerode"; import fs from "fs"; import logger from "./logger"; - import { dockerConfig, target } from "../typings/dockerConfig"; function loadDockerConfig(): dockerConfig { @@ -11,16 +10,15 @@ function loadDockerConfig(): dockerConfig { logger.debug("Refreshed DockerConfig.json"); return JSON.parse(rawData) as dockerConfig; } catch (error: unknown) { - logger.error( - "Error loading dockerConfig.json: " + (error as Error).message, - ); - throw new Error("Failed to load Docker configuration"); + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + throw new Error(errorMsg); } } function createDockerClient(hostConfig: target): Docker { logger.info( - `Creating Docker client for host: ${hostConfig.url} on port: ${hostConfig.port || 2375}`, + `Creating Docker client for host: ${hostConfig.url} on port: ${hostConfig.port || 2375}` ); return new Docker({ host: hostConfig.url, @@ -29,7 +27,7 @@ function createDockerClient(hostConfig: target): Docker { }); } -const getDockerClient = (hostName: string): Docker => { +export const getDockerClient = (hostName: string): Docker => { logger.debug(`Getting Docker Client for ${hostName}`); const config = loadDockerConfig(); const hostConfig = config.hosts.find((host) => host.name === hostName); @@ -41,5 +39,3 @@ const getDockerClient = (hostName: string): Docker => { } return createDockerClient(hostConfig); }; - -export default getDockerClient; diff --git a/src/utils/extractHostData.ts b/src/utils/extractHostData.ts index 0af612e..a383dc0 100644 --- a/src/utils/extractHostData.ts +++ b/src/utils/extractHostData.ts @@ -1,9 +1,54 @@ import { JsonData } from "../typings/hostData"; +import logger from "./logger"; type ComponentMap = Record; -// Export the function with type annotations -function extractRelevantData(jsonData: JsonData) { +interface RelevantData { + hostName: string; + info: { + ID: string; + Containers: number; + ContainersRunning: number; + ContainersPaused: number; + ContainersStopped: number; + Images: number; + OperatingSystem: string; + KernelVersion: string; + Architecture: string; + MemTotal: number; + NCPU: number; + }; + version: { + Components: ComponentMap; + }; +} + +function processComponents(components: unknown): ComponentMap { + try { + if (!Array.isArray(components)) return {}; + + return components.reduce((acc, component) => { + if ( + typeof component === 'object' && + component !== null && + 'Name' in component && + 'Version' in component + ) { + const { Name, Version } = component; + if (typeof Name === 'string' && typeof Version === 'string') { + acc[Name] = Version; + } + } + return acc; + }, {}); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.error(`Error processing components: ${errorMessage}`); + return {}; + } +} + +export function extractRelevantData(jsonData: JsonData): RelevantData { return { hostName: jsonData.hostName, info: { @@ -20,29 +65,7 @@ function extractRelevantData(jsonData: JsonData) { NCPU: jsonData.info.NCPU, }, version: { - Components: (() => { - try { - if (!Array.isArray(jsonData?.version?.Components)) { - return {}; - } - - return jsonData.version.Components.reduce( - (acc, component) => { - if ( - typeof component?.Name === "string" && - typeof component?.Version === "string" - ) { - acc[component.Name] = component.Version; - } - return acc; - }, - {}, - ); - } catch (error) { - console.error("Error processing Components data:", error); - return {}; - } - })(), + Components: processComponents(jsonData?.version?.Components), }, }; } diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 00adbdf..2fd67bd 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -1,7 +1,7 @@ import { createLogger, format, transports } from "winston"; import DailyRotateFile from "winston-daily-rotate-file"; +import { LOG_LEVEL } from "../config/variables"; -// ANSI color codes for log level customization const colors = { gray: "\x1b[90m", reset: "\x1b[0m", @@ -12,7 +12,6 @@ const colors = { blue: "\x1b[34m", }; -// Custom formatter to colorize log levels function colorizeLogLevel(level: string, levelName: string) { switch (level) { case "info": @@ -28,7 +27,7 @@ function colorizeLogLevel(level: string, levelName: string) { } } -// Filter out unwanted logs (example: Exit listeners logs) +// Filter out Exit listeners logs const filterLogs = format((info) => { if ( typeof info.message === "string" && @@ -39,9 +38,8 @@ const filterLogs = format((info) => { return info; }); -// Logger instance const logger = createLogger({ - level: "debug", + level: LOG_LEVEL, format: format.combine( filterLogs(), format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), @@ -56,7 +54,7 @@ const logger = createLogger({ info.level.toLowerCase(), level, ); - const message = `${colors.white}${info.message}${colors.reset}`; + const message = `${colors.white}${(info.message as string).replace(/\n|\r/g, "")}${colors.reset}`; return `${timestamp} ${levelColorized} : ${message}`; }), diff --git a/src/utils/notifications/_template.ts b/src/utils/notifications/_template.ts index 250f095..fd5d71e 100644 --- a/src/utils/notifications/_template.ts +++ b/src/utils/notifications/_template.ts @@ -14,7 +14,8 @@ function getTemplate(): Template | null { const data = fs.readFileSync(templatePath, "utf8"); return JSON.parse(data); } catch (error: unknown) { - logger.error("Failed to load template:", error as Error); + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); return null; } } @@ -28,7 +29,8 @@ function setTemplate(newTemplate: string): void { ); logger.debug("Template updated successfully"); } catch (error: unknown) { - logger.error("Failed to update template:", error as Error); + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } } @@ -65,7 +67,8 @@ function renderTemplate(containerId: string): string | null { return text.replace(new RegExp(`{{${key}}}`, "g"), String(value)); }, template.text); } catch (error: unknown) { - logger.error("Failed to load containers:", error as Error); + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); return null; } } diff --git a/src/utils/notifications/email.ts b/src/utils/notifications/email.ts index 4cd41a1..62b37d3 100644 --- a/src/utils/notifications/email.ts +++ b/src/utils/notifications/email.ts @@ -47,6 +47,7 @@ export async function emailNotification(containerId: string) { try { await transporter.sendMail(mailOptions); } catch (error: unknown) { - logger.error("Error sending email:", error as Error); + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } } diff --git a/src/utils/startServer.ts b/src/utils/startServer.ts new file mode 100644 index 0000000..7ca612f --- /dev/null +++ b/src/utils/startServer.ts @@ -0,0 +1,18 @@ +import { Express } from "express"; +import { Server } from 'http'; +import { startMasterNode } from "../controllers/highAvailability"; +import writeUserConf from "../config/hostsystem"; +import initFiles from "../config/initFiles"; + + +export function startServer(app: Express, server: Server, port: number) { + if (process.env.NODE_ENV === "testing") { + writeUserConf(port); + initFiles(); + } + + + server.listen(port, () => { + startMasterNode(); + }); +} \ No newline at end of file diff --git a/src/utils/swaggerDocs.ts b/src/utils/swaggerDocs.ts index 540304a..7ed90d9 100644 --- a/src/utils/swaggerDocs.ts +++ b/src/utils/swaggerDocs.ts @@ -1,11 +1,12 @@ import swaggerUi from "swagger-ui-express"; -import swaggerJsdoc from "swagger-jsdoc"; -import swaggerConfig from "../config/swaggerConfig"; +import { options } from "../config/swaggerConfig"; +import yaml from "yamljs"; import express from "express"; +import { SwaggerDefinition } from "swagger-jsdoc"; const swaggerDocs = (app: express.Application) => { - const specs = swaggerJsdoc(swaggerConfig); - app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(specs)); + const swaggerYaml: SwaggerDefinition = yaml.load("./src/config/swagger.yaml"); + app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerYaml, options)); }; export default swaggerDocs; diff --git a/src/utils/webSocket.ts b/src/utils/webSocket.ts new file mode 100644 index 0000000..893647e --- /dev/null +++ b/src/utils/webSocket.ts @@ -0,0 +1,113 @@ +import { Server } from 'http'; +import { WebSocketServer, WebSocket } from 'ws'; +import { URL } from 'url'; +import fs from 'fs'; +import logger from "./logger"; +import { streamContainerData } from './containerService'; + +export function setupWebSocket(server: Server) { + const wss = new WebSocketServer({ noServer: true }); + + server.on('upgrade', (req, socket, head) => { + logger.debug(`Received upgrade request for URL: ${req.url}`); + const baseURL = `http://${req.headers.host}/`; + const requestURL = new URL(req.url || '', baseURL); + const {pathname} = requestURL; + logger.debug(`Parsed pathname: ${pathname}`); + + // Debug log to verify path handling + logger.debug(`Handling upgrade for path: ${pathname}`); + + if (pathname === '/wss/container-data' || pathname === '/wss/server-logs') { + wss.handleUpgrade(req, socket, head, (ws) => { + wss.emit('connection', ws, req); + }); + } else { + logger.warn(`Rejected WebSocket connection to invalid path: ${pathname}`); + socket.write('HTTP/1.1 404 Not Found\r\n\r\n'); + socket.destroy(); + } + }); + + server.on("error", (error) => { + logger.error("HTTP server error:", error); + }); + + logger.debug("WebSocket server attached to HTTP server"); + + wss.on('connection', (ws: WebSocket, req) => { + const baseURL = `http://${req.headers.host}/`; + const requestURL = new URL(req.url || '', baseURL); + const {pathname} = requestURL; + + logger.info(`WebSocket connection established to ${pathname}`); + + const handleError = (error: string) => { + ws.send(JSON.stringify({ error })); + ws.close(); + }; + + if (pathname === '/wss/container-data') { + const hostName = requestURL.searchParams.get('host'); + if (!hostName) { + handleError('Missing required host parameter'); + return; + } + streamContainerData(ws, hostName); + } else if (pathname === '/wss/server-logs') { + const logFiles = fs.readdirSync("logs/").filter(file => file.startsWith('app-')); + + if (logFiles.length === 0) { + console.error('No log files found'); + return; + } + + const sortedLogFiles = logFiles.sort((a, b) => { + const dateA = a.match(/\d{4}-\d{2}-\d{2}/)?.[0] ?? ""; + const dateB = b.match(/\d{4}-\d{2}-\d{2}/)?.[0] ?? ""; + + return dateB.localeCompare(dateA); + }); + + const logPath = "logs/" + sortedLogFiles[0]; + + if (!fs.existsSync(logPath)) { + handleError('Log file not found'); + logger.error(`Log file ${logPath} not found`) + return; + } + + // Read the initial content of the log file + let lastSize = fs.statSync(logPath).size; + const history = fs.readFileSync(logPath, 'utf-8'); + ws.send(JSON.stringify({ type: 'log-history', data: history })); + + // Watch the log file for changes + const watcher = fs.watch(logPath, (eventType) => { + if (eventType === 'change') { + const newSize = fs.statSync(logPath).size; + if (newSize > lastSize) { + const stream = fs.createReadStream(logPath, { + start: lastSize, + end: newSize - 1, + encoding: 'utf-8' + }); + + stream.on('data', (chunk) => { + ws.send(JSON.stringify({ type: 'log-update', data: chunk })); + }); + + lastSize = newSize; + } + } + }); + + ws.on('close', () => { + watcher.close(); + logger.info('Closed WebSocket connection for logs'); + }); + } else { + handleError('Invalid WebSocket endpoint'); + } + }); +} \ No newline at end of file diff --git a/tests/main.spec.ts b/tests/main.spec.ts deleted file mode 100644 index f900642..0000000 --- a/tests/main.spec.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { test, expect } from '@playwright/test'; -import ora from 'ora'; - -interface Route { - url: string; -} - -interface FrontendRoute { - url: string; - type: string; -} - -const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); - -test('Swagger - Auth enable and disable', async ({ page }) => { - await page.goto('http://localhost:9876/api-docs/'); - await page.getByLabel('post /auth/enable').click(); - await page.getByRole('button', { name: 'Try it out' }).click(); - await page.getByPlaceholder('password').click(); - await page.getByPlaceholder('password').fill('1'); - await page.getByRole('button', { name: 'Execute' }).click(); - await page.getByRole('button', { name: 'Authorize' }).click(); - await page.getByLabel('Value:').click(); - await page.getByLabel('Value:').fill('1'); - await page.getByLabel('Apply credentials').click(); - await page.getByRole('button', { name: 'Close' }).click(); - await page.getByLabel('post /auth/disable').click(); - await page.getByRole('button', { name: 'Try it out' }).click(); - await page.getByRole('row', { name: 'password *required (query)', exact: true }).getByPlaceholder('password').click(); - await page.getByRole('row', { name: 'password *required (query)', exact: true }).getByPlaceholder('password').fill('1'); - await page.locator('#operations-Authentication-post_auth_disable').getByRole('button', { name: 'Execute' }).click(); -}); - -test('Return 200 status code', async ({ request }) => { - await sleep(5000); - const getRoutes: Route[] = [ - { url: 'http://localhost:9876/data/latest' }, - { url: 'http://localhost:9876/data/time/24h' }, - { url: 'http://localhost:9876/api/hosts' }, - { url: 'http://localhost:9876/api/host/Fin-2/stats' }, - { url: 'http://localhost:9876/api/containers' }, - { url: 'http://localhost:9876/api/config' }, - { url: 'http://localhost:9876/api/current-schedule' }, - { url: 'http://localhost:9876/api/frontend-config' }, - { url: 'http://localhost:9876/api/status' }, - { url: 'http://localhost:9876/ha/config' }, - { url: 'http://localhost:9876/ha/prepare-sync' }, - { url: 'http://localhost:9876/notification-service/get-template' } - ]; - - for (const { url } of getRoutes) { - const spinner = ora(`Checking: ${url}`).start(); - const response = await request.get(`${url}`); - await sleep(1000); - if (response.status() === 200) { - spinner.succeed(`Checked: ${url}`); - } else { - spinner.fail(`Failed: ${url}`); - } - expect(response.status()).toBe(200); - } - - const putRoutes: Route[] = [ - { url: 'http://localhost:9876/conf/addHost?name=test&url=localhost&port=2375' }, - { url: 'http://localhost:9876/conf/scheduler?interval=300s' } - ]; - - for (const { url } of putRoutes) { - const spinner = ora(`Checking: ${url}`).start(); - const response = await request.put(`${url}`); - await sleep(1000); - if (response.status() === 200) { - spinner.succeed(`Checked: ${url}`); - } else { - spinner.fail(`Failed: ${url}`); - } - expect(response.status()).toBe(200); - } - - const data = { text: "{{name}} ({{id}}) on {{hostName}} is {{state}}." }; - - const spinner = ora('Checking: http://localhost:9876/notification-service/set-template').start(); - const response = await request.post('http://localhost:9876/notification-service/set-template', { data }); - await sleep(1000); - if (response.status() === 200) { - spinner.succeed('Checked: http://localhost:9876/notification-service/set-template'); - } else { - spinner.fail('Failed: http://localhost:9876/notification-service/set-template'); - } - expect(response.status()).toBe(200); - - // Remove test host: - const deleteSpinner = ora('Removing test host').start(); - await request.delete('http://localhost:9876/conf/removeHost?hostName=test'); - await sleep(1000); - deleteSpinner.succeed('Removed test host'); - - const frontendRoutes: FrontendRoute[] = [ - { url: 'http://localhost:9876/frontend/tag/test/test', type: "post" }, - { url: 'http://localhost:9876/frontend/pin/test', type: "post" }, - { url: 'http://localhost:9876/frontend/add-link/test/https%3A%2F%2Fexample.com', type: "post" }, - { url: 'http://localhost:9876/frontend/add-icon/test/test.png/true', type: "post" }, - { url: 'http://localhost:9876/frontend/hide/test', type: "delete" }, - { url: 'http://localhost:9876/frontend/remove-tag/test/test', type: "delete" }, - { url: 'http://localhost:9876/frontend/remove-link/test', type: "delete" }, - { url: 'http://localhost:9876/frontend/show/test', type: "post" }, - { url: 'http://localhost:9876/frontend/remove-icon/test', type: "delete" }, - { url: 'http://localhost:9876/frontend/unpin/test', type: "delete" } - ]; - - for (const { url, type } of frontendRoutes) { - const spinner = ora(`Checking: ${url}`).start(); - let response; - if (type === "post") { - response = await request.post(`${url}`); - } else if (type === "put") { - response = await request.put(`${url}`); - } else if (type === "delete") { - response = await request.delete(`${url}`); - } else { - throw new Error(`Unsupported request type: ${type}`); - } - await sleep(1000); - if (response.status() === 200) { - spinner.succeed(`Checked: ${url}`); - } else { - spinner.fail(`Failed: ${url}`); - } - expect(response.status()).toBe(200); - } -}); diff --git a/tsconfig.json b/tsconfig.json index 4af6b1d..c4f6f4c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,6 @@ }, "$schema": "https://json.schemastore.org/tsconfig", "display": "Recommended", - "include": ["src/**/*"], + "include": ["src/**/*", "**/*.d.ts", "__tests__/**/*"], "exclude": ["node_modules", "**/*.spec.ts"] } From ad79836fd0bb0a6047a71fd3d613fb8378bec997 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 9 Feb 2025 13:39:26 +0000 Subject: [PATCH 117/324] Automatically added GitHub issue links to TODOs --- src/handlers/graph.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/handlers/graph.ts b/src/handlers/graph.ts index 53e245f..bf33d22 100644 --- a/src/handlers/graph.ts +++ b/src/handlers/graph.ts @@ -101,6 +101,7 @@ async function generateGraphFiles( for (const [hostName, containers] of Object.entries(allContainerData)) { if ("error" in containers) { // TODO: make error'ed hosts better + // Issue URL: https://github.com/Its4Nik/DockStatAPI/issues/32 graphElements.push({ data: { id: hostName, From 1aa44c6efda580d02004314b48e6abb417f95075 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sun, 9 Feb 2025 20:06:46 +0100 Subject: [PATCH 118/324] Fix: Websocket logic adjustments --- src/handlers/graph.ts | 4 ++-- src/utils/webSocket.ts | 33 ++++++++++++++------------------- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/src/handlers/graph.ts b/src/handlers/graph.ts index bf33d22..6212adf 100644 --- a/src/handlers/graph.ts +++ b/src/handlers/graph.ts @@ -77,14 +77,14 @@ async function renderGraphToImage( } } - throw new Error(`Graph rendering failed: ${errorMessage}`); + throw new Error(`Graph rendering failed - ${errorMessage}`); } finally { if (browser) { await browser.close().catch(() => { }); } } - logger.info(`Graph rendered and image saved to: ${outputImagePath}`); + logger.info(`Graph rendered and image saved to - ${outputImagePath}`); } async function generateGraphFiles( diff --git a/src/utils/webSocket.ts b/src/utils/webSocket.ts index 893647e..0d41229 100644 --- a/src/utils/webSocket.ts +++ b/src/utils/webSocket.ts @@ -12,7 +12,7 @@ export function setupWebSocket(server: Server) { logger.debug(`Received upgrade request for URL: ${req.url}`); const baseURL = `http://${req.headers.host}/`; const requestURL = new URL(req.url || '', baseURL); - const {pathname} = requestURL; + const { pathname } = requestURL; logger.debug(`Parsed pathname: ${pathname}`); // Debug log to verify path handling @@ -38,7 +38,7 @@ export function setupWebSocket(server: Server) { wss.on('connection', (ws: WebSocket, req) => { const baseURL = `http://${req.headers.host}/`; const requestURL = new URL(req.url || '', baseURL); - const {pathname} = requestURL; + const { pathname } = requestURL; logger.info(`WebSocket connection established to ${pathname}`); @@ -83,27 +83,22 @@ export function setupWebSocket(server: Server) { ws.send(JSON.stringify({ type: 'log-history', data: history })); // Watch the log file for changes - const watcher = fs.watch(logPath, (eventType) => { - if (eventType === 'change') { - const newSize = fs.statSync(logPath).size; - if (newSize > lastSize) { - const stream = fs.createReadStream(logPath, { - start: lastSize, - end: newSize - 1, - encoding: 'utf-8' - }); - - stream.on('data', (chunk) => { - ws.send(JSON.stringify({ type: 'log-update', data: chunk })); - }); - - lastSize = newSize; - } + const watcher = fs.watchFile(logPath, { interval: 1000 }, (curr, prev) => { + if (curr.size > prev.size) { + const stream = fs.createReadStream(logPath, { + start: prev.size, + end: curr.size - 1, + encoding: 'utf-8' + }); + + stream.on('data', (chunk) => { + ws.send(JSON.stringify({ type: 'log-update', data: chunk })); + }); } }); ws.on('close', () => { - watcher.close(); + watcher.removeAllListeners(); logger.info('Closed WebSocket connection for logs'); }); } else { From 915cc33fe4d9431b1a3b5165b4d97eb44da5e8e9 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sun, 9 Feb 2025 20:09:18 +0100 Subject: [PATCH 119/324] Fix: Make Linter happy --- src/utils/webSocket.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/utils/webSocket.ts b/src/utils/webSocket.ts index 0d41229..cabf3be 100644 --- a/src/utils/webSocket.ts +++ b/src/utils/webSocket.ts @@ -78,7 +78,6 @@ export function setupWebSocket(server: Server) { } // Read the initial content of the log file - let lastSize = fs.statSync(logPath).size; const history = fs.readFileSync(logPath, 'utf-8'); ws.send(JSON.stringify({ type: 'log-history', data: history })); From 63c396e571789bc1e823a0bfdfbc4281ec4d35fc Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 11 Feb 2025 13:25:19 +0100 Subject: [PATCH 120/324] Change: Graph generation logic changed! --- src/handlers/graph.ts | 295 +++++++--------------------------- src/routes/graphs/routes.ts | 14 ++ src/utils/containerService.ts | 60 +++---- 3 files changed, 108 insertions(+), 261 deletions(-) diff --git a/src/handlers/graph.ts b/src/handlers/graph.ts index 6212adf..61607c1 100644 --- a/src/handlers/graph.ts +++ b/src/handlers/graph.ts @@ -2,257 +2,84 @@ import cytoscape from "cytoscape"; import logger from "../utils/logger"; import { AllContainerData, ContainerData } from "./../typings/dockerConfig"; import { atomicWrite } from "../utils/atomicWrite"; -import { rateLimitedReadFile } from "../utils/rateLimitFS"; const CACHE_DIR_JSON = "./src/data/graph.json"; -const CACHE_DIR_HTML = "./src/data/graph.html"; -const _assets = "./src/utils/assets"; -const serverSvg = `${_assets}/server-icon.svg`; -const containerSvg = `${_assets}/container-icon.svg`; -const pngPath = "./src/data/graph.png"; -async function getPathData(path: string) { - try { - return await rateLimitedReadFile(path); - - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - return false; - } -} - -async function renderGraphToImage( - htmlContent: string, - outputImagePath: string, -): Promise { - let puppeteer; - try { - puppeteer = await import("puppeteer"); - } catch (error) { - logger.error("Puppeteer is not installed. Please install it to generate images."); - throw new Error(`Puppeteer is not installed (${error})`); - } - - let browser; - try { - browser = await puppeteer.default.launch({ - headless: "shell", - args: ["--disable-setuid-sandbox", "--no-sandbox"], - executablePath: process.env.PUPPETEER_EXECUTABLE_PATH, - }); - - const page = await browser.newPage(); - await page.setContent(htmlContent, { waitUntil: "networkidle0" }); - await page.waitForSelector("#cy", { visible: true, timeout: 15000 }); - - await page.waitForFunction( - () => { - const cyElement = document.querySelector("#cy"); - return cyElement ? cyElement.children.length > 0 : false; - }, - { timeout: 10000 } - ); - - await page.screenshot({ - path: outputImagePath, - type: outputImagePath.endsWith(".jpg") ? "jpeg" : "png", - fullPage: true, - captureBeyondViewport: true, - }); - } catch (error: unknown) { - let errorMessage = "Unknown error occurred during browser operation"; - - if (error instanceof Error) { - errorMessage = error.message; - - // Detect common dependency errors - if (errorMessage.includes("libnss3") || errorMessage.includes("libxcb")) { - errorMessage = `❗ Missing system dependencies (libnss3)`; - } - - // Detect Chrome not found errors - if (errorMessage.includes("Failed to launch")) { - errorMessage = `❗ Chrome not found!`; - } - } - - throw new Error(`Graph rendering failed - ${errorMessage}`); - } finally { - if (browser) { - await browser.close().catch(() => { }); - } - } - - logger.info(`Graph rendered and image saved to - ${outputImagePath}`); -} - -async function generateGraphFiles( +async function generateGraphJSON( allContainerData: AllContainerData, ): Promise { - if (process.env.CI === "true") { - logger.warn("Running inside a CI/CD Action, wont generated graphs"); - return false; - } else { - try { - logger.info("generateGraphFiles >>> Starting generation"); - const graphElements: cytoscape.ElementDefinition[] = []; - - for (const [hostName, containers] of Object.entries(allContainerData)) { - if ("error" in containers) { - // TODO: make error'ed hosts better - // Issue URL: https://github.com/Its4Nik/DockStatAPI/issues/32 - graphElements.push({ + try { + logger.info("generateGraphJSON >>> Starting generation"); + + // Define the new JSON structure + const graphData = { + nodes: [] as cytoscape.ElementDefinition[], + edges: [] as cytoscape.ElementDefinition[], + }; + + for (const [hostName, containers] of Object.entries(allContainerData)) { + if ("error" in containers) { + graphData.nodes.push({ + data: { + id: hostName, + label: `Host: ${hostName} Error: ${containers.error}`, + type: "server", + error: true, + }, + }); + } else { + const containerList = containers as ContainerData[]; + + // Host node with container count and metadata + graphData.nodes.push({ + data: { + id: hostName, + label: `${hostName}\n${containerList.length} Containers`, + type: "server", + hostName, + containerCount: containerList.length, + }, + }); + + for (const container of containerList) { + const { id, ...otherContainerProps } = container; + + graphData.nodes.push({ data: { - id: hostName, - label: `Host: ${hostName} Error: ${containers.error}`, - type: "server", + id: id, + label: `${container.name}\n${container.state.toUpperCase()}`, + type: "container", + parent: hostName, + ...otherContainerProps, }, }); - } else { - const containerList = containers as ContainerData[]; - // host node with container count - graphElements.push({ + // Edge between host and container + graphData.edges.push({ data: { - id: hostName, - label: `${hostName} - ${containerList.length} Containers`, - type: "server", + id: `${hostName}-${container.id}`, + source: hostName, + target: container.id, + connectionType: "host-container", }, }); - - for (const container of containerList) { - // container node - graphElements.push({ - data: { - id: container.id, - label: `${container.name} (${container.state})`, - type: "container", - }, - }); - - // edge between host and container - graphElements.push({ - data: { - source: hostName, - target: container.id, - }, - }); - } } } - - atomicWrite(CACHE_DIR_JSON, JSON.stringify(graphElements, null, 2)); - - const htmlContent = ` - - - - - - Cytoscape Graph - - - - -
- - - - `; - - atomicWrite(CACHE_DIR_HTML, htmlContent); - await renderGraphToImage(htmlContent, pngPath) - .then(() => logger.debug("HTML converted to image successfully!")) - .catch((err) => logger.error("Error:", err)); - - logger.info("generateGraphFiles <<< Files generated successfully"); - return true; - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - return false; } + + // Write the new structured JSON to file + atomicWrite(CACHE_DIR_JSON, JSON.stringify(graphData, null, 2)); + logger.info("generateGraphJSON <<< JSON file generated successfully"); + return true; + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + return false; } } -function getGraphFilePaths() { - return { json: CACHE_DIR_JSON, html: CACHE_DIR_HTML }; +function getGraphFilePath() { + return { json: CACHE_DIR_JSON }; } -export { generateGraphFiles, getGraphFilePaths }; +export { generateGraphJSON, getGraphFilePath }; diff --git a/src/routes/graphs/routes.ts b/src/routes/graphs/routes.ts index db53205..bf6c8a9 100644 --- a/src/routes/graphs/routes.ts +++ b/src/routes/graphs/routes.ts @@ -1,6 +1,7 @@ import { Request, Response, Router } from "express"; import { createResponseHandler } from "../../handlers/response"; import path from "path"; +import { rateLimitedReadFile } from "../../utils/rateLimitFS"; const router = Router(); router.get("/", async (req: Request, res: Response) => { @@ -28,4 +29,17 @@ router.get("/image", async (req: Request, res: Response) => { } }); +router.get("/json", async (req: Request, res: Response) => { + const ResponseHandler = createResponseHandler(res); + try { + const data = await rateLimitedReadFile( + path.join(__dirname, "/../../.." + "/src/data/graph.json"), + ); + return ResponseHandler.rawData(data, "Graph JSON fetched"); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } +}); + export default router; diff --git a/src/utils/containerService.ts b/src/utils/containerService.ts index 86dc2d3..0bb0a4e 100644 --- a/src/utils/containerService.ts +++ b/src/utils/containerService.ts @@ -1,18 +1,18 @@ import logger from "./logger"; -import { ContainerInfo, } from "dockerode"; +import { ContainerInfo } from "dockerode"; import { getDockerClient } from "./dockerClient"; import fs from "fs"; import { atomicWrite } from "./atomicWrite"; const configPath = "./src/data/dockerConfig.json"; import { AllContainerData, HostConfig } from "../typings/dockerConfig"; -import { generateGraphFiles } from "../handlers/graph"; +import { generateGraphJSON } from "../handlers/graph"; import { WebSocket } from "ws"; export function loadConfig() { try { if (!fs.existsSync(configPath)) { logger.warn( - `Config file not found. Creating an empty file at ${configPath}` + `Config file not found. Creating an empty file at ${configPath}`, ); atomicWrite(configPath, JSON.stringify({ hosts: [] }, null, 2)); } @@ -37,7 +37,9 @@ export async function fetchContainersForHost(hostName: string) { try { const docker = getDockerClient(hostName); - const containers: ContainerInfo[] = await docker.listContainers({ all: true }); + const containers: ContainerInfo[] = await docker.listContainers({ + all: true, + }); return await Promise.all( containers.map(async (container) => { @@ -56,7 +58,8 @@ export async function fetchContainersForHost(hostName: string) { containerStats.precpu_stats.system_cpu_usage; const cpuUsage = systemCpuDelta > 0 - ? (cpuDelta / systemCpuDelta) * containerStats.cpu_stats.online_cpus + ? (cpuDelta / systemCpuDelta) * + containerStats.cpu_stats.online_cpus : 0; return { @@ -90,7 +93,7 @@ export async function fetchContainersForHost(hostName: string) { networkMode: "unknown", }; } - }) + }), ); } catch (error) { logger.error(`Error fetching containers for ${hostName}: ${error}`); @@ -105,16 +108,18 @@ export async function fetchAllContainers(): Promise { await Promise.all( config.hosts.map(async (hostConfig: HostConfig) => { try { - allContainerData[hostConfig.name] = await fetchContainersForHost(hostConfig.name); + allContainerData[hostConfig.name] = await fetchContainersForHost( + hostConfig.name, + ); } catch (error) { allContainerData[hostConfig.name] = { - error: `Error fetching containers: ${error instanceof Error ? error.message : String(error)}` + error: `Error fetching containers: ${error instanceof Error ? error.message : String(error)}`, }; } - }) + }), ); - generateGraphFiles(allContainerData); + generateGraphJSON(allContainerData); return allContainerData; } @@ -127,12 +132,14 @@ export async function streamContainerData(ws: WebSocket, hostName: string) { const eventStream = await docker.getEvents(); // eslint-disable-next-line - if (!(eventStream instanceof require('stream').Readable)) { - throw new Error('Failed to get valid event stream'); + if (!(eventStream instanceof require("stream").Readable)) { + throw new Error("Failed to get valid event stream"); } const handleData = (chunk: Buffer) => { - ws.send(JSON.stringify({ type: "container-event", data: chunk.toString() })); + ws.send( + JSON.stringify({ type: "container-event", data: chunk.toString() }), + ); }; const handleError = (err: Error) => { @@ -140,28 +147,27 @@ export async function streamContainerData(ws: WebSocket, hostName: string) { ws.close(); }; - eventStream - .on('data', handleData) - .on('error', handleError); + eventStream.on("data", handleData).on("error", handleError); const closeHandler = () => { eventStream - .removeListener('data', handleData) - .removeListener('error', handleError) - .removeListener('closed', handleError); + .removeListener("data", handleData) + .removeListener("error", handleError) + .removeListener("closed", handleError); logger.info(`Closed event stream for ${hostName}`); }; - ws.on('close', closeHandler); - ws.on('error', closeHandler); - + ws.on("close", closeHandler); + ws.on("error", closeHandler); } catch (error) { const message = error instanceof Error ? error.message : String(error); logger.error("Container data error:", message); - ws.send(JSON.stringify({ - error: "Failed to fetch container data", - details: message - })); + ws.send( + JSON.stringify({ + error: "Failed to fetch container data", + details: message, + }), + ); ws.close(); } -} \ No newline at end of file +} From 3f6792325b5f7a2e22b72d25ee0671ec43f9a5ee Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 12 Feb 2025 13:14:27 +0100 Subject: [PATCH 121/324] Fix: Code styling --- .github/workflows/build-image.yaml | 2 +- .github/workflows/validation.yaml | 2 +- CREDITS.md | 43 +++--- TODO.md | 4 +- __tests__/auth.spec.ts | 4 +- __tests__/config.spec.ts | 2 +- __tests__/database.spec.ts | 2 +- __tests__/frontend.spec.ts | 4 +- __tests__/getters.spec.ts | 2 +- src/handlers/graph.ts | 1 - src/handlers/stack.ts | 2 +- src/utils/dockerClient.ts | 2 +- src/utils/extractHostData.ts | 8 +- src/utils/startServer.ts | 6 +- src/utils/webSocket.ts | 208 +++++++++++++++-------------- 15 files changed, 148 insertions(+), 144 deletions(-) diff --git a/.github/workflows/build-image.yaml b/.github/workflows/build-image.yaml index bbb4875..9d43ff1 100644 --- a/.github/workflows/build-image.yaml +++ b/.github/workflows/build-image.yaml @@ -14,7 +14,7 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 - + - name: Set up QEMU uses: docker/setup-qemu-action@v3 diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index 7e2b685..52797fc 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -1,6 +1,6 @@ name: "Run all tests" -on: +on: push: release: types: diff --git a/CREDITS.md b/CREDITS.md index 50b66ab..6dd2d89 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -20,35 +20,37 @@ This file shows all npm packages used in DockStatAPI (also Dev packages) | ------------------------------------ | ------------------------------------------------------------------------ | -------------------- | | @ampproject/remapping@2.3.0 | https://github.com/ampproject/remapping | Justin Ridgewell | | @balena/dockerignore@1.0.2 | https://github.com/balena-io-modules/dockerignore | N/A | -| @eslint/config-array@0.19.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/core@0.9.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/object-schema@2.1.5 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/plugin-kit@0.2.4 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/config-array@0.19.2 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/core@0.10.0 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/core@0.11.0 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/object-schema@2.1.6 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/plugin-kit@0.2.5 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @grpc/grpc-js@1.12.6 | https://github.com/grpc/grpc-node/tree/master/packages/grpc-js | Google Inc. | +| @grpc/proto-loader@0.7.13 | https://github.com/grpc/grpc-node | Google Inc. | | @humanfs/core@0.19.1 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | | @humanfs/node@0.16.6 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | | @humanwhocodes/module-importer@1.0.1 | https://github.com/humanwhocodes/module-importer | Nicholas C. Zaks | | @humanwhocodes/retry@0.3.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | | @humanwhocodes/retry@0.4.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | -| @puppeteer/browsers@2.7.0 | https://github.com/puppeteer/puppeteer/tree/main/packages/browsers | The Chromium Authors | +| @puppeteer/browsers@2.7.1 | https://github.com/puppeteer/puppeteer/tree/main/packages/browsers | The Chromium Authors | | @scarf/scarf@1.4.0 | https://github.com/scarf-sh/scarf-js | Scarf Systems | | @sigstore/bundle@3.0.0 | https://github.com/sigstore/sigstore-js | bdehamer@github.com | | @sigstore/core@2.0.0 | https://github.com/sigstore/sigstore-js | bdehamer@github.com | -| @sigstore/protobuf-specs@0.3.2 | https://github.com/sigstore/protobuf-specs | bdehamer@github.com | +| @sigstore/protobuf-specs@0.3.3 | https://github.com/sigstore/protobuf-specs | bdehamer@github.com | | @sigstore/sign@3.0.0 | https://github.com/sigstore/sigstore-js | bdehamer@github.com | | @sigstore/tuf@3.0.0 | https://github.com/sigstore/sigstore-js | bdehamer@github.com | | @sigstore/verify@2.0.0 | https://github.com/sigstore/sigstore-js | bdehamer@github.com | | b4a@1.6.7 | https://github.com/holepunchto/b4a | Holepunch | | bare-events@2.5.4 | https://github.com/holepunchto/bare-events | Holepunch | -| bare-fs@2.3.5 | https://github.com/holepunchto/bare-fs | Holepunch | -| bare-os@2.4.4 | https://github.com/holepunchto/bare-os | Holepunch | -| bare-path@2.1.3 | https://github.com/holepunchto/bare-path | Holepunch | -| bare-stream@2.6.1 | https://github.com/holepunchto/bare-stream | Holepunch | +| bare-fs@4.0.1 | https://github.com/holepunchto/bare-fs | Holepunch | +| bare-os@3.4.0 | https://github.com/holepunchto/bare-os | Holepunch | +| bare-path@3.0.0 | https://github.com/holepunchto/bare-path | Holepunch | +| bare-stream@2.6.5 | https://github.com/holepunchto/bare-stream | Holepunch | | bser@2.1.1 | https://github.com/facebook/watchman | Wez Furlong | -| chromium-bidi@0.11.0 | https://github.com/GoogleChromeLabs/chromium-bidi | The Chromium Authors | -| chromium-bidi@0.12.0 | https://github.com/GoogleChromeLabs/chromium-bidi | The Chromium Authors | +| chromium-bidi@1.2.0 | https://github.com/GoogleChromeLabs/chromium-bidi | The Chromium Authors | | detect-libc@2.0.3 | https://github.com/lovell/detect-libc | Lovell Fuller | -| docker-modem@5.0.3 | https://github.com/apocas/docker-modem | Pedro Dias | -| dockerode@4.0.2 | https://github.com/apocas/dockerode | Pedro Dias | +| docker-modem@5.0.6 | https://github.com/apocas/docker-modem | Pedro Dias | +| dockerode@4.0.4 | https://github.com/apocas/dockerode | Pedro Dias | | ejs@3.1.10 | https://github.com/mde/ejs | Matthew Eernisse | | eslint-visitor-keys@3.4.3 | https://github.com/eslint/eslint-visitor-keys | Toru Nagashima | | eslint-visitor-keys@4.2.0 | https://github.com/eslint/js | Toru Nagashima | @@ -57,14 +59,15 @@ This file shows all npm packages used in DockStatAPI (also Dev packages) | filelist@1.0.4 | https://github.com/mde/filelist | Matthew Eernisse | | human-signals@2.1.0 | https://github.com/ehmicky/human-signals | ehmicky | | jake@10.9.2 | https://github.com/jakejs/jake | Matthew Eernisse | -| puppeteer-core@24.0.0 | https://github.com/puppeteer/puppeteer/tree/main/packages/puppeteer-core | The Chromium Authors | -| puppeteer@24.0.0 | https://github.com/puppeteer/puppeteer/tree/main/packages/puppeteer | The Chromium Authors | +| long@5.2.4 | https://github.com/dcodeIO/long.js | Daniel Wirtz | +| puppeteer-core@24.2.0 | https://github.com/puppeteer/puppeteer/tree/main/packages/puppeteer-core | The Chromium Authors | +| puppeteer@24.2.0 | https://github.com/puppeteer/puppeteer/tree/main/packages/puppeteer | The Chromium Authors | | sigstore@3.0.0 | https://github.com/sigstore/sigstore-js | bdehamer@github.com | | spdx-correct@3.2.0 | https://github.com/jslicense/spdx-correct.js | N/A | -| swagger-ui-dist@5.18.2 | https://github.com/swagger-api/swagger-ui | N/A | +| swagger-ui-dist@5.18.3 | https://github.com/swagger-api/swagger-ui | N/A | | text-decoder@1.2.3 | https://github.com/holepunchto/text-decoder | Holepunch | | tunnel-agent@0.6.0 | https://github.com/mikeal/tunnel-agent | Mikeal Rogers | -| typescript@5.7.2 | https://github.com/microsoft/TypeScript | Microsoft Corp. | +| typescript@5.7.3 | https://github.com/microsoft/TypeScript | Microsoft Corp. | | validate-npm-package-license@3.0.4 | https://github.com/kemitchell/validate-npm-package-license.js | Kyle E. Mitchell | | walker@1.0.8 | https://github.com/daaku/nodejs-walker | Naitik Shah | @@ -72,7 +75,7 @@ This file shows all npm packages used in DockStatAPI (also Dev packages) | Name | Repository | Publisher | | ---------- | -------------------------- | ----------- | -| npm@11.0.0 | https://github.com/npm/cli | GitHub Inc. | +| npm@11.1.0 | https://github.com/npm/cli | GitHub Inc. | ### License: BlueOak-1.0.0 @@ -94,7 +97,7 @@ This file shows all npm packages used in DockStatAPI (also Dev packages) | Name | Repository | Publisher | | ------------------------- | -------------------------------------------- | ---------- | -| caniuse-lite@1.0.30001690 | https://github.com/browserslist/caniuse-lite | Ben Briggs | +| caniuse-lite@1.0.30001698 | https://github.com/browserslist/caniuse-lite | Ben Briggs | ### License: Python-2.0 diff --git a/TODO.md b/TODO.md index 44a128d..b850ba7 100644 --- a/TODO.md +++ b/TODO.md @@ -7,12 +7,12 @@ - [x] Structure code differently - [x] Write new README and make the docs better - [x] Update more files to correct TS syntax => remove "any" -- [X] Websockets +- [x] Websockets - [x] Better /api/status endpoint with connection status of each host - [x] Update notification service - [x] Adjust process.env variables since they don't really work as expected (See [commit](https://github.com/Its4Nik/dockstatapi/pull/21/commits/a03b58c7a17e269f46216df5492e18d008774961)) - [ ] Better project structure - [x] Update logging => Better errors - [x] Update json responses -- [X] Swagger update +- [x] Swagger update - [ ] Edge case testing diff --git a/__tests__/auth.spec.ts b/__tests__/auth.spec.ts index bcf0eb2..84c5f04 100644 --- a/__tests__/auth.spec.ts +++ b/__tests__/auth.spec.ts @@ -1,5 +1,5 @@ export const testPass = "123456789"; -import { Server } from 'http'; +import { Server } from "http"; import supertest from "supertest"; import { startServer } from "../src/utils/startServer"; import app from "../src/server"; @@ -35,4 +35,4 @@ describe("Authentication", () => { expect(res.status).toEqual(200); expect(res.type).toEqual(expect.stringContaining("json")); }); -}); \ No newline at end of file +}); diff --git a/__tests__/config.spec.ts b/__tests__/config.spec.ts index d635600..2650e9e 100644 --- a/__tests__/config.spec.ts +++ b/__tests__/config.spec.ts @@ -1,7 +1,7 @@ import supertest from "supertest"; import { startServer } from "../src/utils/startServer"; import app from "../src/server"; -import { Server } from 'http'; +import { Server } from "http"; const port = 13002; const server = new Server(app); diff --git a/__tests__/database.spec.ts b/__tests__/database.spec.ts index c0c46c1..55102ce 100644 --- a/__tests__/database.spec.ts +++ b/__tests__/database.spec.ts @@ -1,7 +1,7 @@ import supertest from "supertest"; import { startServer } from "../src/utils/startServer"; import app from "../src/server"; -import { Server } from 'http'; +import { Server } from "http"; const port = 13003; const server = new Server(app); diff --git a/__tests__/frontend.spec.ts b/__tests__/frontend.spec.ts index 753b98d..af25adc 100644 --- a/__tests__/frontend.spec.ts +++ b/__tests__/frontend.spec.ts @@ -1,7 +1,7 @@ import supertest from "supertest"; import { startServer } from "../src/utils/startServer"; import app from "../src/server"; -import { Server } from 'http'; +import { Server } from "http"; const port = 13004; const server = new Server(app); @@ -29,8 +29,6 @@ const verifiedResponse = [ }, ]; - - describe("Test frontend specific configurations", () => { it( "Setup the configuration file", diff --git a/__tests__/getters.spec.ts b/__tests__/getters.spec.ts index 3ba5950..f951f42 100644 --- a/__tests__/getters.spec.ts +++ b/__tests__/getters.spec.ts @@ -2,7 +2,7 @@ import { createPreviousResponse } from "./util/previousResponse"; import supertest from "supertest"; import { startServer } from "../src/utils/startServer"; import app from "../src/server"; -import { Server } from 'http'; +import { Server } from "http"; const port = 13005; const server = new Server(app); diff --git a/src/handlers/graph.ts b/src/handlers/graph.ts index 61607c1..587d576 100644 --- a/src/handlers/graph.ts +++ b/src/handlers/graph.ts @@ -49,7 +49,6 @@ async function generateGraphJSON( id: id, label: `${container.name}\n${container.state.toUpperCase()}`, type: "container", - parent: hostName, ...otherContainerProps, }, }); diff --git a/src/handlers/stack.ts b/src/handlers/stack.ts index e87b533..b3daa0f 100644 --- a/src/handlers/stack.ts +++ b/src/handlers/stack.ts @@ -107,7 +107,7 @@ class StackHandler { async stackCompose(req: Request, res: Response) { const ResponseHandler = createResponseHandler(res); try { - const {name} = req.params; + const { name } = req.params; return ResponseHandler.rawData( await getStackCompose(name), "Stack compose fetched", diff --git a/src/utils/dockerClient.ts b/src/utils/dockerClient.ts index 469c409..ff77088 100644 --- a/src/utils/dockerClient.ts +++ b/src/utils/dockerClient.ts @@ -18,7 +18,7 @@ function loadDockerConfig(): dockerConfig { function createDockerClient(hostConfig: target): Docker { logger.info( - `Creating Docker client for host: ${hostConfig.url} on port: ${hostConfig.port || 2375}` + `Creating Docker client for host: ${hostConfig.url} on port: ${hostConfig.port || 2375}`, ); return new Docker({ host: hostConfig.url, diff --git a/src/utils/extractHostData.ts b/src/utils/extractHostData.ts index a383dc0..992f963 100644 --- a/src/utils/extractHostData.ts +++ b/src/utils/extractHostData.ts @@ -29,13 +29,13 @@ function processComponents(components: unknown): ComponentMap { return components.reduce((acc, component) => { if ( - typeof component === 'object' && + typeof component === "object" && component !== null && - 'Name' in component && - 'Version' in component + "Name" in component && + "Version" in component ) { const { Name, Version } = component; - if (typeof Name === 'string' && typeof Version === 'string') { + if (typeof Name === "string" && typeof Version === "string") { acc[Name] = Version; } } diff --git a/src/utils/startServer.ts b/src/utils/startServer.ts index 7ca612f..52dcc25 100644 --- a/src/utils/startServer.ts +++ b/src/utils/startServer.ts @@ -1,18 +1,16 @@ import { Express } from "express"; -import { Server } from 'http'; +import { Server } from "http"; import { startMasterNode } from "../controllers/highAvailability"; import writeUserConf from "../config/hostsystem"; import initFiles from "../config/initFiles"; - export function startServer(app: Express, server: Server, port: number) { if (process.env.NODE_ENV === "testing") { writeUserConf(port); initFiles(); } - server.listen(port, () => { startMasterNode(); }); -} \ No newline at end of file +} diff --git a/src/utils/webSocket.ts b/src/utils/webSocket.ts index cabf3be..66d1f74 100644 --- a/src/utils/webSocket.ts +++ b/src/utils/webSocket.ts @@ -1,107 +1,113 @@ -import { Server } from 'http'; -import { WebSocketServer, WebSocket } from 'ws'; -import { URL } from 'url'; -import fs from 'fs'; +import { Server } from "http"; +import { WebSocketServer, WebSocket } from "ws"; +import { URL } from "url"; +import fs from "fs"; import logger from "./logger"; -import { streamContainerData } from './containerService'; +import { streamContainerData } from "./containerService"; export function setupWebSocket(server: Server) { - const wss = new WebSocketServer({ noServer: true }); - - server.on('upgrade', (req, socket, head) => { - logger.debug(`Received upgrade request for URL: ${req.url}`); - const baseURL = `http://${req.headers.host}/`; - const requestURL = new URL(req.url || '', baseURL); - const { pathname } = requestURL; - logger.debug(`Parsed pathname: ${pathname}`); - - // Debug log to verify path handling - logger.debug(`Handling upgrade for path: ${pathname}`); - - if (pathname === '/wss/container-data' || pathname === '/wss/server-logs') { - wss.handleUpgrade(req, socket, head, (ws) => { - wss.emit('connection', ws, req); - }); - } else { - logger.warn(`Rejected WebSocket connection to invalid path: ${pathname}`); - socket.write('HTTP/1.1 404 Not Found\r\n\r\n'); - socket.destroy(); - } - }); - - server.on("error", (error) => { - logger.error("HTTP server error:", error); - }); - - logger.debug("WebSocket server attached to HTTP server"); - - wss.on('connection', (ws: WebSocket, req) => { - const baseURL = `http://${req.headers.host}/`; - const requestURL = new URL(req.url || '', baseURL); - const { pathname } = requestURL; - - logger.info(`WebSocket connection established to ${pathname}`); - - const handleError = (error: string) => { - ws.send(JSON.stringify({ error })); - ws.close(); - }; - - if (pathname === '/wss/container-data') { - const hostName = requestURL.searchParams.get('host'); - if (!hostName) { - handleError('Missing required host parameter'); - return; - } - streamContainerData(ws, hostName); - } else if (pathname === '/wss/server-logs') { - const logFiles = fs.readdirSync("logs/").filter(file => file.startsWith('app-')); - - if (logFiles.length === 0) { - console.error('No log files found'); - return; - } - - const sortedLogFiles = logFiles.sort((a, b) => { - const dateA = a.match(/\d{4}-\d{2}-\d{2}/)?.[0] ?? ""; - const dateB = b.match(/\d{4}-\d{2}-\d{2}/)?.[0] ?? ""; - - return dateB.localeCompare(dateA); - }); - - const logPath = "logs/" + sortedLogFiles[0]; - - if (!fs.existsSync(logPath)) { - handleError('Log file not found'); - logger.error(`Log file ${logPath} not found`) - return; - } - - // Read the initial content of the log file - const history = fs.readFileSync(logPath, 'utf-8'); - ws.send(JSON.stringify({ type: 'log-history', data: history })); - - // Watch the log file for changes - const watcher = fs.watchFile(logPath, { interval: 1000 }, (curr, prev) => { - if (curr.size > prev.size) { - const stream = fs.createReadStream(logPath, { - start: prev.size, - end: curr.size - 1, - encoding: 'utf-8' - }); - - stream.on('data', (chunk) => { - ws.send(JSON.stringify({ type: 'log-update', data: chunk })); - }); - } + const wss = new WebSocketServer({ noServer: true }); + + server.on("upgrade", (req, socket, head) => { + logger.debug(`Received upgrade request for URL: ${req.url}`); + const baseURL = `http://${req.headers.host}/`; + const requestURL = new URL(req.url || "", baseURL); + const { pathname } = requestURL; + logger.debug(`Parsed pathname: ${pathname}`); + + // Debug log to verify path handling + logger.debug(`Handling upgrade for path: ${pathname}`); + + if (pathname === "/wss/container-data" || pathname === "/wss/server-logs") { + wss.handleUpgrade(req, socket, head, (ws) => { + wss.emit("connection", ws, req); + }); + } else { + logger.warn(`Rejected WebSocket connection to invalid path: ${pathname}`); + socket.write("HTTP/1.1 404 Not Found\r\n\r\n"); + socket.destroy(); + } + }); + + server.on("error", (error) => { + logger.error("HTTP server error:", error); + }); + + logger.debug("WebSocket server attached to HTTP server"); + + wss.on("connection", (ws: WebSocket, req) => { + const baseURL = `http://${req.headers.host}/`; + const requestURL = new URL(req.url || "", baseURL); + const { pathname } = requestURL; + + logger.info(`WebSocket connection established to ${pathname}`); + + const handleError = (error: string) => { + ws.send(JSON.stringify({ error })); + ws.close(); + }; + + if (pathname === "/wss/container-data") { + const hostName = requestURL.searchParams.get("host"); + if (!hostName) { + handleError("Missing required host parameter"); + return; + } + streamContainerData(ws, hostName); + } else if (pathname === "/wss/server-logs") { + const logFiles = fs + .readdirSync("logs/") + .filter((file) => file.startsWith("app-")); + + if (logFiles.length === 0) { + console.error("No log files found"); + return; + } + + const sortedLogFiles = logFiles.sort((a, b) => { + const dateA = a.match(/\d{4}-\d{2}-\d{2}/)?.[0] ?? ""; + const dateB = b.match(/\d{4}-\d{2}-\d{2}/)?.[0] ?? ""; + + return dateB.localeCompare(dateA); + }); + + const logPath = "logs/" + sortedLogFiles[0]; + + if (!fs.existsSync(logPath)) { + handleError("Log file not found"); + logger.error(`Log file ${logPath} not found`); + return; + } + + // Read the initial content of the log file + const history = fs.readFileSync(logPath, "utf-8"); + ws.send(JSON.stringify({ type: "log-history", data: history })); + + // Watch the log file for changes + const watcher = fs.watchFile( + logPath, + { interval: 1000 }, + (curr, prev) => { + if (curr.size > prev.size) { + const stream = fs.createReadStream(logPath, { + start: prev.size, + end: curr.size - 1, + encoding: "utf-8", }); - ws.on('close', () => { - watcher.removeAllListeners(); - logger.info('Closed WebSocket connection for logs'); + stream.on("data", (chunk) => { + ws.send(JSON.stringify({ type: "log-update", data: chunk })); }); - } else { - handleError('Invalid WebSocket endpoint'); - } - }); -} \ No newline at end of file + } + }, + ); + + ws.on("close", () => { + watcher.removeAllListeners(); + logger.info("Closed WebSocket connection for logs"); + }); + } else { + handleError("Invalid WebSocket endpoint"); + } + }); +} From d7c80167cfc64d525454ea9ae0a6811c022a89cb Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sat, 15 Feb 2025 19:13:17 +0100 Subject: [PATCH 122/324] Fix: Remove unusable routes --- src/config/swagger.yaml | 11 ----------- src/handlers/graph.ts | 2 -- src/routes/graphs/routes.ts | 25 ------------------------- 3 files changed, 38 deletions(-) diff --git a/src/config/swagger.yaml b/src/config/swagger.yaml index 9a1d50f..2230f73 100644 --- a/src/config/swagger.yaml +++ b/src/config/swagger.yaml @@ -33,17 +33,6 @@ info: - Multi Arch Docker builds through docker buildx - High Availability using single master and unlimited worker nodes! -
- Your container graph - [Interactive Graph](http://localhost:9876/graph) - - [Raw image](http://localhost:9876/graph/image) - - --- - - ![Your container graph](http://localhost:9876/graph/image) -
- # 🔗 DockStatAPI v2 Documentation _⚠️ = Deprecation warning_ diff --git a/src/handlers/graph.ts b/src/handlers/graph.ts index 587d576..12e0572 100644 --- a/src/handlers/graph.ts +++ b/src/handlers/graph.ts @@ -11,7 +11,6 @@ async function generateGraphJSON( try { logger.info("generateGraphJSON >>> Starting generation"); - // Define the new JSON structure const graphData = { nodes: [] as cytoscape.ElementDefinition[], edges: [] as cytoscape.ElementDefinition[], @@ -66,7 +65,6 @@ async function generateGraphJSON( } } - // Write the new structured JSON to file atomicWrite(CACHE_DIR_JSON, JSON.stringify(graphData, null, 2)); logger.info("generateGraphJSON <<< JSON file generated successfully"); return true; diff --git a/src/routes/graphs/routes.ts b/src/routes/graphs/routes.ts index bf6c8a9..fcaa798 100644 --- a/src/routes/graphs/routes.ts +++ b/src/routes/graphs/routes.ts @@ -4,31 +4,6 @@ import path from "path"; import { rateLimitedReadFile } from "../../utils/rateLimitFS"; const router = Router(); -router.get("/", async (req: Request, res: Response) => { - const ResponseHandler = createResponseHandler(res); - try { - const graphPath = path.join( - __dirname, - "/../../.." + "/src/data/graph.html", - ); - return res.contentType("html").status(200).sendFile(graphPath); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } -}); - -router.get("/image", async (req: Request, res: Response) => { - const ResponseHandler = createResponseHandler(res); - try { - const graphPath = path.join(__dirname, "/../../.." + "/src/data/graph.png"); - return res.contentType("image/png").status(200).sendFile(graphPath); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } -}); - router.get("/json", async (req: Request, res: Response) => { const ResponseHandler = createResponseHandler(res); try { From 62b05740c178821753c4ef609755e3c57f133c5a Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sat, 15 Feb 2025 21:55:51 +0100 Subject: [PATCH 123/324] Fix: Add docker executable --- docker/Dockerfile-base | 5 +++-- docker/Dockerfile-dev | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index 76cec4c..d135eaa 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -30,8 +30,9 @@ FROM node:20-alpine AS production WORKDIR /api -RUN apk add --no-cache bash curl && \ - adduser -h /api -s /bin/bash -D dockstatapi +RUN apk add --no-cache bash curl docker-cli && \ + adduser -h /api -s /bin/bash -D dockstatapi && \ + addgroup dockstatapi docker HEALTHCHECK --interval=5m --timeout=3s \ CMD curl -f http://localhost:9876/api/status || exit 1 diff --git a/docker/Dockerfile-dev b/docker/Dockerfile-dev index 43a4240..bcc54e4 100644 --- a/docker/Dockerfile-dev +++ b/docker/Dockerfile-dev @@ -30,6 +30,10 @@ FROM node:20-alpine AS production WORKDIR /api +RUN apk add --no-cache bash curl docker-cli && \ + adduser -h /api -s /bin/bash -D dockstatapi && \ + addgroup dockstatapi docker + RUN apk add --no-cache bash curl && \ adduser -h /api -s /bin/bash -D dockstatapi From 01d73071c6b6fa6ab29e39207a9aef20657ddbd5 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sat, 15 Feb 2025 22:06:49 +0100 Subject: [PATCH 124/324] Fix: Fixing user creation and docker user --- docker/Dockerfile-base | 14 +++++++------- docker/Dockerfile-dev | 17 +++++++---------- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index d135eaa..80b1ae8 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -15,9 +15,7 @@ WORKDIR /app ENV NODE_NO_WARNINGS=1 -RUN apk add --no-cache bash - -COPY tsconfig.json environment.d.ts package*.json ./ +COPY package*.json tsconfig.json environment.d.ts ./ RUN npm install --production=false @@ -30,8 +28,9 @@ FROM node:20-alpine AS production WORKDIR /api -RUN apk add --no-cache bash curl docker-cli && \ - adduser -h /api -s /bin/bash -D dockstatapi && \ +RUN apk add --no-cache docker-cli bash curl && \ + adduser -h /api -s /bin/sh -D dockstatapi && \ + addgroup -S docker && \ addgroup dockstatapi docker HEALTHCHECK --interval=5m --timeout=3s \ @@ -42,7 +41,8 @@ COPY --chown=dockstatapi:dockstatapi --from=builder /app/package*.json /api/ COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/config/swagger.yaml /api/src/config/swagger.yaml COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/utils/assets /api/src/utils/assets -RUN npm install --omit=dev +RUN npm install --omit=dev && \ + rm -rf package-lock.json node_modules/.cache COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/misc/entrypoint.sh /api/entrypoint.sh COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/misc/createEnvFile.sh /api/createEnvFile.sh @@ -56,4 +56,4 @@ RUN mkdir -p /api/src/data && \ STOPSIGNAL 130 USER dockstatapi -ENTRYPOINT [ "bash", "./entrypoint.sh", "--prod" ] +ENTRYPOINT [ "sh", "./entrypoint.sh", "--prod" ] diff --git a/docker/Dockerfile-dev b/docker/Dockerfile-dev index bcc54e4..7b43940 100644 --- a/docker/Dockerfile-dev +++ b/docker/Dockerfile-dev @@ -15,9 +15,7 @@ WORKDIR /app ENV NODE_NO_WARNINGS=1 -RUN apk add --no-cache bash - -COPY tsconfig.json environment.d.ts package*.json ./ +COPY package*.json tsconfig.json environment.d.ts ./ RUN npm install --production=false @@ -30,13 +28,11 @@ FROM node:20-alpine AS production WORKDIR /api -RUN apk add --no-cache bash curl docker-cli && \ - adduser -h /api -s /bin/bash -D dockstatapi && \ +RUN apk add --no-cache docker-cli bash curl && \ + adduser -h /api -s /bin/sh -D dockstatapi && \ + addgroup -S docker && \ addgroup dockstatapi docker -RUN apk add --no-cache bash curl && \ - adduser -h /api -s /bin/bash -D dockstatapi - HEALTHCHECK --interval=5m --timeout=3s \ CMD curl -f http://localhost:9876/api/status || exit 1 @@ -45,7 +41,8 @@ COPY --chown=dockstatapi:dockstatapi --from=builder /app/package*.json /api/ COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/config/swagger.yaml /api/src/config/swagger.yaml COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/utils/assets /api/src/utils/assets -RUN npm install +RUN npm install --omit=dev && \ + rm -rf package-lock.json node_modules/.cache COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/misc/entrypoint.sh /api/entrypoint.sh COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/misc/createEnvFile.sh /api/createEnvFile.sh @@ -59,4 +56,4 @@ RUN mkdir -p /api/src/data && \ STOPSIGNAL 130 USER dockstatapi -ENTRYPOINT [ "bash", "./entrypoint.sh", "--dev" ] +ENTRYPOINT [ "sh", "./entrypoint.sh", "--dev" ] From 955d41363e0d3057dd23b24f55f89fe49c2ce4cd Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sat, 15 Feb 2025 22:12:11 +0100 Subject: [PATCH 125/324] Fix: Use Node-20-slim in build stage --- docker/Dockerfile-base | 2 +- docker/Dockerfile-dev | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index 80b1ae8..8dd8929 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -1,5 +1,5 @@ # Stage 1: Build stage -FROM node:20-alpine AS builder +FROM node:20-slim AS builder LABEL maintainer="https://github.com/its4nik" LABEL version="2.0.1" diff --git a/docker/Dockerfile-dev b/docker/Dockerfile-dev index 7b43940..f3f3cae 100644 --- a/docker/Dockerfile-dev +++ b/docker/Dockerfile-dev @@ -1,5 +1,5 @@ # Stage 1: Build stage -FROM node:20-alpine AS builder +FROM node:20-slim AS builder LABEL maintainer="https://github.com/its4nik" LABEL version="2.0.1" From fdac5739dafcea9ac2f14cea8a5c29b5172699d5 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sat, 15 Feb 2025 22:53:43 +0100 Subject: [PATCH 126/324] Fix: Update to composeAction --- src/handlers/stack.ts | 48 ++++++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/src/handlers/stack.ts b/src/handlers/stack.ts index b3daa0f..ad36373 100644 --- a/src/handlers/stack.ts +++ b/src/handlers/stack.ts @@ -27,29 +27,35 @@ export async function validate(name: string): Promise { async function composeAction(option: string, name: string): Promise { const composeFile: string = path.join(PROJECT_ROOT, `stacks/${name}`); - switch (option) { - case "start": { - await compose.upAll({ cwd: composeFile, log: false }).then( - () => { - return true; - }, - (err: unknown) => { - throw new Error(err as string); - }, - ); - break; + try { + switch (option) { + case "start": { + await compose.upAll({ cwd: composeFile, log: false }); + break; + } + case "stop": { + await compose.downAll({ cwd: composeFile, log: false }); + break; + } + default: + throw new Error(`Invalid option: ${option}`); } - case "stop": { - await compose.downAll({ cwd: composeFile, log: false }).then( - () => { - return true; - }, - (err: unknown) => { - throw new Error(err as string); - }, - ); - break; + } catch (err) { + let errorMessage: string; + const portAllocated: string = "port is already allocated"; + + if (err instanceof Error) { + errorMessage = err.message; + } else if (typeof err === "object" && err !== null) { + errorMessage = JSON.stringify(err); + } else { + errorMessage = String(err); + } + + if (errorMessage.search(portAllocated)) { + errorMessage = "Port(s) already allocated"; } + throw new Error(errorMessage); } } From caa4b6ce6ae58a766fea42a1ea27d1f6ae5b6106 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sat, 15 Feb 2025 23:51:45 +0100 Subject: [PATCH 127/324] Fix: Make errors more verbose for debugging purpose --- src/handlers/stack.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/handlers/stack.ts b/src/handlers/stack.ts index ad36373..0f15e16 100644 --- a/src/handlers/stack.ts +++ b/src/handlers/stack.ts @@ -53,7 +53,7 @@ async function composeAction(option: string, name: string): Promise { } if (errorMessage.search(portAllocated)) { - errorMessage = "Port(s) already allocated"; + logger.error("Port(s) already allocated"); } throw new Error(errorMessage); } From fbc63f4e06124facc2ece85507c80ead12da1269 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sun, 16 Feb 2025 00:23:08 +0100 Subject: [PATCH 128/324] Fix: Add correct docker packages to Container --- docker/Dockerfile-base | 2 +- docker/Dockerfile-dev | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index 8dd8929..296dfe1 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -28,7 +28,7 @@ FROM node:20-alpine AS production WORKDIR /api -RUN apk add --no-cache docker-cli bash curl && \ +RUN apk add --no-cache docker docker-compose bash curl && \ adduser -h /api -s /bin/sh -D dockstatapi && \ addgroup -S docker && \ addgroup dockstatapi docker diff --git a/docker/Dockerfile-dev b/docker/Dockerfile-dev index f3f3cae..d0bd623 100644 --- a/docker/Dockerfile-dev +++ b/docker/Dockerfile-dev @@ -28,7 +28,7 @@ FROM node:20-alpine AS production WORKDIR /api -RUN apk add --no-cache docker-cli bash curl && \ +RUN apk add --no-cache docker docker-compose bash curl && \ adduser -h /api -s /bin/sh -D dockstatapi && \ addgroup -S docker && \ addgroup dockstatapi docker From 0dc912eb9b35dc72531b7039455fdab03f816fbc Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sun, 16 Feb 2025 00:40:44 +0100 Subject: [PATCH 129/324] Fix: Adjust dockerfile (gosh i hope it works now) --- docker/Dockerfile-base | 62 ++++++++++++++++++++++++++---------------- docker/Dockerfile-dev | 62 ++++++++++++++++++++++++++---------------- 2 files changed, 78 insertions(+), 46 deletions(-) diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index 296dfe1..bfee3a3 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -1,5 +1,5 @@ # Stage 1: Build stage -FROM node:20-slim AS builder +FROM node:20-alpine AS builder LABEL maintainer="https://github.com/its4nik" LABEL version="2.0.1" @@ -15,45 +15,61 @@ WORKDIR /app ENV NODE_NO_WARNINGS=1 +RUN apk add --no-cache curl bash + COPY package*.json tsconfig.json environment.d.ts ./ -RUN npm install --production=false +RUN npm ci --include=dev COPY ./src ./src RUN mv ./src/sample-variable.json ./src/data/variables.json -RUN npm run build:mini -# Stage 2: Production stage -FROM node:20-alpine AS production +RUN npm run build:mini +# -------------------------------------- +# Stage 2: Dependency pruning stage +FROM node:20-alpine AS deps WORKDIR /api +COPY --from=builder /app/package*.json . +RUN npm ci --omit=dev -RUN apk add --no-cache docker docker-compose bash curl && \ - adduser -h /api -s /bin/sh -D dockstatapi && \ - addgroup -S docker && \ - addgroup dockstatapi docker +# -------------------------------------- +# Stage 3: Final production image +FROM node:20-alpine AS prod -HEALTHCHECK --interval=5m --timeout=3s \ - CMD curl -f http://localhost:9876/api/status || exit 1 +WORKDIR /api -COPY --chown=dockstatapi:dockstatapi --from=builder /app/dist/src /api/src -COPY --chown=dockstatapi:dockstatapi --from=builder /app/package*.json /api/ -COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/config/swagger.yaml /api/src/config/swagger.yaml -COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/utils/assets /api/src/utils/assets +RUN apk add --no-cache docker-cli bash curl && \ + mkdir -p /usr/libexec/docker/cli-plugins && \ + curl -sSL "https://github.com/docker/compose/releases/latest/download/docker-compose-linux-$(uname -m)" \ + -o /usr/libexec/docker/cli-plugins/docker-compose && \ + chmod +x /usr/libexec/docker/cli-plugins/docker-compose && \ + rm -rf /var/cache/apk/* -RUN npm install --omit=dev && \ - rm -rf package-lock.json node_modules/.cache +ARG USER_ID=10001 +ARG GROUP_ID=10001 +RUN addgroup -g $GROUP_ID dockstatapi && \ + adduser -u $USER_ID -G dockstatapi -h /api -s /bin/sh -D dockstatapi -COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/misc/entrypoint.sh /api/entrypoint.sh -COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/misc/createEnvFile.sh /api/createEnvFile.sh -RUN chmod +x /api/*.sh +COPY --from=builder --chown=dockstatapi:dockstatapi /app/dist/src ./src +COPY --from=builder --chown=dockstatapi:dockstatapi /app/src/config/swagger.yaml ./src/config/swagger.yaml +COPY --from=builder --chown=dockstatapi:dockstatapi /app/src/utils/assets ./src/utils/assets +COPY --from=deps --chown=dockstatapi:dockstatapi /api/node_modules ./node_modules -EXPOSE 9876 +COPY --from=builder --chown=dockstatapi:dockstatapi /app/src/misc/entrypoint.sh . +COPY --from=builder --chown=dockstatapi:dockstatapi /app/src/misc/createEnvFile.sh . +RUN chmod +x *.sh RUN mkdir -p /api/src/data && \ - chmod -R 777 /api/src/data /api && \ - chown -R dockstatapi:dockstatapi /api + chown -R dockstatapi:dockstatapi /api && \ + chmod -R 755 /api && \ + chmod 775 /api/src/data + +HEALTHCHECK --interval=5m --timeout=3s \ + CMD curl -f http://localhost:9876/api/status || exit 1 +EXPOSE 9876 STOPSIGNAL 130 USER dockstatapi + ENTRYPOINT [ "sh", "./entrypoint.sh", "--prod" ] diff --git a/docker/Dockerfile-dev b/docker/Dockerfile-dev index d0bd623..dfda535 100644 --- a/docker/Dockerfile-dev +++ b/docker/Dockerfile-dev @@ -1,5 +1,5 @@ # Stage 1: Build stage -FROM node:20-slim AS builder +FROM node:20-alpine AS builder LABEL maintainer="https://github.com/its4nik" LABEL version="2.0.1" @@ -15,45 +15,61 @@ WORKDIR /app ENV NODE_NO_WARNINGS=1 +RUN apk add --no-cache curl bash + COPY package*.json tsconfig.json environment.d.ts ./ -RUN npm install --production=false +RUN npm ci --include=dev COPY ./src ./src RUN mv ./src/sample-variable.json ./src/data/variables.json -RUN npm run build -# Stage 2: Production stage -FROM node:20-alpine AS production +RUN npm run build +# -------------------------------------- +# Stage 2: Dependency pruning stage +FROM node:20-alpine AS deps WORKDIR /api +COPY --from=builder /app/package*.json . +RUN npm ci --omit=dev -RUN apk add --no-cache docker docker-compose bash curl && \ - adduser -h /api -s /bin/sh -D dockstatapi && \ - addgroup -S docker && \ - addgroup dockstatapi docker +# -------------------------------------- +# Stage 3: Final production image +FROM node:20-alpine AS prod -HEALTHCHECK --interval=5m --timeout=3s \ - CMD curl -f http://localhost:9876/api/status || exit 1 +WORKDIR /api -COPY --chown=dockstatapi:dockstatapi --from=builder /app/dist/src /api/src -COPY --chown=dockstatapi:dockstatapi --from=builder /app/package*.json /api/ -COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/config/swagger.yaml /api/src/config/swagger.yaml -COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/utils/assets /api/src/utils/assets +RUN apk add --no-cache docker-cli bash curl && \ + mkdir -p /usr/libexec/docker/cli-plugins && \ + curl -sSL "https://github.com/docker/compose/releases/latest/download/docker-compose-linux-$(uname -m)" \ + -o /usr/libexec/docker/cli-plugins/docker-compose && \ + chmod +x /usr/libexec/docker/cli-plugins/docker-compose && \ + rm -rf /var/cache/apk/* -RUN npm install --omit=dev && \ - rm -rf package-lock.json node_modules/.cache +ARG USER_ID=10001 +ARG GROUP_ID=10001 +RUN addgroup -g $GROUP_ID dockstatapi && \ + adduser -u $USER_ID -G dockstatapi -h /api -s /bin/sh -D dockstatapi -COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/misc/entrypoint.sh /api/entrypoint.sh -COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/misc/createEnvFile.sh /api/createEnvFile.sh -RUN chmod +x /api/*.sh +COPY --from=builder --chown=dockstatapi:dockstatapi /app/dist/src ./src +COPY --from=builder --chown=dockstatapi:dockstatapi /app/src/config/swagger.yaml ./src/config/swagger.yaml +COPY --from=builder --chown=dockstatapi:dockstatapi /app/src/utils/assets ./src/utils/assets +COPY --from=deps --chown=dockstatapi:dockstatapi /api/node_modules ./node_modules -EXPOSE 9876 +COPY --from=builder --chown=dockstatapi:dockstatapi /app/src/misc/entrypoint.sh . +COPY --from=builder --chown=dockstatapi:dockstatapi /app/src/misc/createEnvFile.sh . +RUN chmod +x *.sh RUN mkdir -p /api/src/data && \ - chmod -R 777 /api/src/data /api && \ - chown -R dockstatapi:dockstatapi /api + chown -R dockstatapi:dockstatapi /api && \ + chmod -R 755 /api && \ + chmod 775 /api/src/data + +HEALTHCHECK --interval=5m --timeout=3s \ + CMD curl -f http://localhost:9876/api/status || exit 1 +EXPOSE 9876 STOPSIGNAL 130 USER dockstatapi + ENTRYPOINT [ "sh", "./entrypoint.sh", "--dev" ] From c49da92f649c9e78ccd289929168f8aabe6300ff Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sun, 16 Feb 2025 15:46:33 +0100 Subject: [PATCH 130/324] Fix: Adjust entrypoints and dockerfiles --- docker/Dockerfile-base | 1 + docker/Dockerfile-dev | 1 + src/misc/entrypoint.sh | 1 - 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index bfee3a3..f21146b 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -54,6 +54,7 @@ RUN addgroup -g $GROUP_ID dockstatapi && \ COPY --from=builder --chown=dockstatapi:dockstatapi /app/dist/src ./src COPY --from=builder --chown=dockstatapi:dockstatapi /app/src/config/swagger.yaml ./src/config/swagger.yaml COPY --from=builder --chown=dockstatapi:dockstatapi /app/src/utils/assets ./src/utils/assets +COPY --from=builder /app/package.json ./ COPY --from=deps --chown=dockstatapi:dockstatapi /api/node_modules ./node_modules COPY --from=builder --chown=dockstatapi:dockstatapi /app/src/misc/entrypoint.sh . diff --git a/docker/Dockerfile-dev b/docker/Dockerfile-dev index dfda535..00b8800 100644 --- a/docker/Dockerfile-dev +++ b/docker/Dockerfile-dev @@ -54,6 +54,7 @@ RUN addgroup -g $GROUP_ID dockstatapi && \ COPY --from=builder --chown=dockstatapi:dockstatapi /app/dist/src ./src COPY --from=builder --chown=dockstatapi:dockstatapi /app/src/config/swagger.yaml ./src/config/swagger.yaml COPY --from=builder --chown=dockstatapi:dockstatapi /app/src/utils/assets ./src/utils/assets +COPY --from=builder /app/package.json ./ COPY --from=deps --chown=dockstatapi:dockstatapi /api/node_modules ./node_modules COPY --from=builder --chown=dockstatapi:dockstatapi /app/src/misc/entrypoint.sh . diff --git a/src/misc/entrypoint.sh b/src/misc/entrypoint.sh index 77b6236..b352ca7 100755 --- a/src/misc/entrypoint.sh +++ b/src/misc/entrypoint.sh @@ -1,4 +1,3 @@ -# entrypoint.sh: #!/bin/bash VERSION="$(cat ./package.json | grep version | cut -d '"' -f 4)" From 9576cdc972022b2968012dc298c82b7635325582 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sun, 16 Feb 2025 15:51:54 +0100 Subject: [PATCH 131/324] Feat: Test new workflow --- .github/workflows/validation.yaml | 238 ++++++++---------------------- 1 file changed, 63 insertions(+), 175 deletions(-) diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index 52797fc..dfa18ed 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -1,20 +1,18 @@ -name: "Run all tests" +name: "CI/CD Pipeline" on: push: release: - types: - - published + types: [published] jobs: validation: + name: "Code Validation & Tests" runs-on: ubuntu-24.04 - name: "Validation" permissions: - security-events: write - packages: read actions: read contents: read + packages: read steps: - name: Checkout uses: actions/checkout@v4 @@ -31,77 +29,37 @@ jobs: - name: Create varaibles.json run: npm run local-env-file - - name: Run prettier + - name: Run code formatting run: npm run prettier - name: Run linter run: npm run lint - - name: Build + - name: Build project run: npm run build:mini - name: Audit packages run: npm audit --audit-level=high - - name: Jests + - name: Run tests run: npm run test:silent - ToDo: - needs: validation - runs-on: ubuntu-20.04 - name: "ToDo comment to issue" - permissions: - contents: write - issues: write - pull-requests: write - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: "TODO to Issue" - uses: "alstr/todo-to-issue-action@v5" - with: - INSERT_ISSUE_URLS: "true" - - - name: Set Git user - run: | - git config --global user.name "github-actions[bot]" - git config --global user.email "github-actions[bot]@users.noreply.github.com" - - - name: Commit and Push Changes - run: | - git add -A - if [[ `git status --porcelain` ]]; then - git commit -m "Automatically added GitHub issue links to TODOs" - git push - else - echo "No changes to commit" - fi - - CodeQL: - needs: [ToDo] + security-analysis: + name: "Security Analysis" runs-on: ubuntu-24.04 - name: "Analyze TypeScript" + needs: validation permissions: security-events: write - packages: read - actions: read contents: read - + packages: read steps: - name: Checkout repository uses: actions/checkout@v4 - - name: Setup python - uses: actions/setup-python@v5 - with: - python-version: "3.13" - - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: javascript-typescript - build-mode: none queries: security-extended config: | query-filter: @@ -110,194 +68,124 @@ jobs: - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 - with: - category: "/language:javascript-typescript" - Anchore: - needs: [ToDo] + container-scanning: + name: "Container Security" runs-on: ubuntu-24.04 - name: "Anchore" + needs: validation permissions: security-events: write - packages: read - actions: read contents: read steps: - - name: Set up Grype installation path - run: echo "$HOME/bin" >> $GITHUB_PATH - - - name: Setup python - uses: actions/setup-python@v5 - with: - python-version: "3.13" + - name: Checkout repository + uses: actions/checkout@v4 - name: Download Grype run: | curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b $HOME/bin + echo "$HOME/bin" >> $GITHUB_PATH - - uses: actions/checkout@v4 - - - name: Build the Container image + - name: Build Docker image run: docker build . --file docker/Dockerfile-base --tag localbuild/testimage:latest - - name: Run Grype test + - name: Run vulnerability scan run: grype -o sarif localbuild/testimage:latest > results.sarif - - name: Upload Anchore scan SARIF report + - name: Upload SARIF report uses: github/codeql-action/upload-sarif@v3 with: sarif_file: ./results.sarif - test-building: - needs: [ToDo] + build-test: + name: "Docker Build Test" runs-on: ubuntu-24.04 - name: "Test building" + needs: validation permissions: - security-events: write + contents: read packages: read - actions: read - contents: write steps: - name: Checkout repository uses: actions/checkout@v4 - - name: Setup python - uses: actions/setup-python@v5 - with: - python-version: "3.13" - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Login to Github Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Generate Docker tags - uses: docker/metadata-action@v5 - id: metadata - with: - images: ghcr.io/${{ github.repository }} - tags: | - type=raw,enable=true,priority=200,prefix=,suffix=,value=${{ github.sha }} - - - name: Build and Push Docker Images + - name: Build Docker image uses: docker/build-push-action@v6 with: context: . file: docker/Dockerfile-base platforms: linux/amd64,linux/arm64 push: false - tags: ${{ steps.metadata.outputs.tags }} - labels: ${{ steps.metadata.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max - build-dev: - name: "Dev-build" - permissions: - security-events: read - packages: write - actions: read - contents: read + todo-management: + name: "TODO Issue Management" runs-on: ubuntu-24.04 - if: github.ref_name == 'dev' - needs: [test-building, Anchore, CodeQL] + needs: validation + permissions: + contents: write + issues: write steps: - - name: Checkout Repository - uses: actions/checkout@v3 - - - name: Setup python - uses: actions/setup-python@v5 - with: - python-version: "3.13" - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to Github Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ github.token }} + - name: Checkout repository + uses: actions/checkout@v4 - - name: Generate Docker tags - uses: docker/metadata-action@v5 - id: metadata + - name: Process TODOs + uses: alstr/todo-to-issue-action@v5 with: - images: ghcr.io/${{ github.repository }} - tags: | - type=raw,enable=true,priority=200,prefix=,suffix=,value=nightly - flavor: | - latest=false + INSERT_ISSUE_URLS: "true" - - name: Build and Push Docker Images - uses: docker/build-push-action@v6 - with: - context: . - file: docker/Dockerfile-dev - platforms: linux/amd64,linux/arm64 - push: true - tags: ${{ steps.metadata.outputs.tags }} - labels: ${{ steps.metadata.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max + - name: Commit changes + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git add -A + if [[ $(git status --porcelain) ]]; then + git commit -m "Automatically process TODOs [skip ci]" + git push + fi - build-pre-release: - name: "Pre-Release-build" + deployment: + name: "Docker Deployment" + runs-on: ubuntu-24.04 + needs: [security-analysis, container-scanning, build-test] permissions: - security-events: read packages: write - actions: read contents: read - runs-on: ubuntu-24.04 - if: "github.event.release.prerelease" - needs: [test-building, Anchore, CodeQL] + strategy: + matrix: + type: [dev, pre-release] steps: - - name: Checkout Repository - uses: actions/checkout@v3 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + - name: Checkout repository + uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Login to Github Container Registry + - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} - password: ${{ github.token }} + password: ${{ secrets.GITHUB_TOKEN }} - - name: Generate Docker tags + - name: Determine tags + id: tags uses: docker/metadata-action@v5 - id: metadata with: images: ghcr.io/${{ github.repository }} tags: | - type=raw,enable=true,priority=200,prefix=,suffix=,value=pre - flavor: | - latest=false + type=raw,enable=${{ matrix.type == 'dev' && github.ref_name == 'dev' || matrix.type == 'pre-release' && github.event.release.prerelease }},value=${{ matrix.type == 'dev' && 'nightly' || 'pre' }} - - name: Build and Push Docker Images + - name: Build and push uses: docker/build-push-action@v6 with: context: . file: docker/Dockerfile-dev platforms: linux/amd64,linux/arm64 - push: true - tags: ${{ steps.metadata.outputs.tags }} - labels: ${{ steps.metadata.outputs.labels }} + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.tags.outputs.tags }} + labels: ${{ steps.tags.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max From 749c28653a34d8ab8dabdb2670bbc0f9f0b8e241 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sun, 16 Feb 2025 16:02:57 +0100 Subject: [PATCH 132/324] Feat: Test new workflow --- .github/workflows/validation.yaml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index dfa18ed..d4349e9 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -155,7 +155,11 @@ jobs: contents: read strategy: matrix: - type: [dev, pre-release] + type: [dev, pre-release, release] + if: > + (matrix.type == 'dev' && github.ref_name == 'dev') + || (matrix.type == 'pre-release' && github.event_name == 'release' && github.event.release.prerelease) + || (matrix.type == 'release' && github.event_name == 'release' && !github.event.release.prerelease) steps: - name: Checkout repository uses: actions/checkout@v4 @@ -176,7 +180,7 @@ jobs: with: images: ghcr.io/${{ github.repository }} tags: | - type=raw,enable=${{ matrix.type == 'dev' && github.ref_name == 'dev' || matrix.type == 'pre-release' && github.event.release.prerelease }},value=${{ matrix.type == 'dev' && 'nightly' || 'pre' }} + type=raw,enable=${{ matrix.type == 'dev' && github.ref_name == 'dev' || matrix.type == 'pre-release' && github.event_name == 'release' && github.event.release.prerelease || matrix.type == 'release' && github.event_name == 'release' && !github.event.release.prerelease }},value=${{ matrix.type == 'dev' && 'nightly' || matrix.type == 'pre-release' && 'pre' || matrix.type == 'release' && 'latest' }} - name: Build and push uses: docker/build-push-action@v6 From dec86d312c5c146d2c3dbba46dae41de7ecc54a3 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sun, 16 Feb 2025 16:06:19 +0100 Subject: [PATCH 133/324] Fix: test new workflow --- .github/workflows/validation.yaml | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index d4349e9..9a9ec93 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -155,19 +155,33 @@ jobs: contents: read strategy: matrix: - type: [dev, pre-release, release] - if: > - (matrix.type == 'dev' && github.ref_name == 'dev') - || (matrix.type == 'pre-release' && github.event_name == 'release' && github.event.release.prerelease) - || (matrix.type == 'release' && github.event_name == 'release' && !github.event.release.prerelease) + include: + - type: dev + # Only enable when pushing to the dev branch + enabled: ${{ github.ref_name == 'dev' }} + - type: pre-release + # Only enable when a release event is published and it's a prerelease + enabled: ${{ github.event_name == 'release' && github.event.release.prerelease }} + - type: release + # Only enable when a release event is published and it's NOT a prerelease + enabled: ${{ github.event_name == 'release' && !github.event.release.prerelease }} steps: + - name: Exit early if deployment is not enabled + if: ${{ !matrix.enabled }} + run: | + echo "Skipping deployment for matrix type '${{ matrix.type }}' because conditions are not met." + exit 0 + - name: Checkout repository + if: ${{ matrix.enabled }} uses: actions/checkout@v4 - name: Set up Docker Buildx + if: ${{ matrix.enabled }} uses: docker/setup-buildx-action@v3 - name: Login to GitHub Container Registry + if: ${{ matrix.enabled }} uses: docker/login-action@v3 with: registry: ghcr.io @@ -175,14 +189,16 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Determine tags + if: ${{ matrix.enabled }} id: tags uses: docker/metadata-action@v5 with: images: ghcr.io/${{ github.repository }} tags: | - type=raw,enable=${{ matrix.type == 'dev' && github.ref_name == 'dev' || matrix.type == 'pre-release' && github.event_name == 'release' && github.event.release.prerelease || matrix.type == 'release' && github.event_name == 'release' && !github.event.release.prerelease }},value=${{ matrix.type == 'dev' && 'nightly' || matrix.type == 'pre-release' && 'pre' || matrix.type == 'release' && 'latest' }} + type=raw,value=${{ matrix.type == 'dev' && 'nightly' || matrix.type == 'pre-release' && 'pre' || matrix.type == 'release' && 'latest' }} - name: Build and push + if: ${{ matrix.enabled }} uses: docker/build-push-action@v6 with: context: . From b0b5b161bcfe8b30ef95ce806d7e2778eb02ccb4 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sun, 23 Feb 2025 09:06:51 +0100 Subject: [PATCH 134/324] Feat: Switch to bun / ElysiaJS --- .dockerignore | 151 - .github/DockStat-dark.png | Bin 82847 -> 0 bytes .github/DockStat.png | Bin 79885 -> 0 bytes .github/workflows/build-image.yaml | 63 - .github/workflows/cloc.yaml | 27 - .github/workflows/remove-stale.yaml | 17 - .github/workflows/validation.yaml | 211 - .gitignore | 172 +- .npmrc | 1 - .nvmrc | 1 - CREDITS.md | 106 - LICENSE | 28 - README.md | 72 +- TODO.md | 18 - __tests__/auth.spec.ts | 38 - __tests__/config.spec.ts | 49 - __tests__/database.spec.ts | 35 - __tests__/frontend.spec.ts | 123 - __tests__/getters.spec.ts | 99 - __tests__/util/previousResponse.ts | 23 - bun.lock | 119 + docker/Dockerfile-base | 76 - docker/Dockerfile-dev | 76 - docker/docker-compose.dev.yaml | 40 - docker/docker-compose.yaml | 82 - environment.d.ts | 12 - eslint.config.mjs | 12 - nodemon.json | 14 - package-lock.json | 13317 ---------------- package.json | 124 +- src/config/db.ts | 23 - src/config/hostsystem.ts | 93 - src/config/initFiles.ts | 42 - src/config/stacks.ts | 260 - src/config/swagger.yaml | 2084 --- src/config/swaggerConfig.ts | 10 - src/config/swaggerTheme.ts | 6 - src/config/variables.ts | 26 - src/controllers/auth.ts | 64 - src/controllers/containerController.ts | 54 - src/controllers/databaseMigration.ts | 20 - src/controllers/fetchData.ts | 76 - src/controllers/frontendConfiguration.ts | 297 - src/controllers/highAvailability.ts | 285 - src/controllers/notificationController.ts | 60 - src/controllers/proxy.ts | 15 - src/controllers/scheduler.ts | 91 - src/core/database/repository.ts | 74 + src/core/docker/host-manager.ts | 38 + src/core/plugins/loader.ts | 21 + src/core/plugins/plugin-manager.ts | 35 + src/core/utils/logger.ts | 72 + src/data/frontendConfiguration.json | 1 - src/data/template.json | 3 - src/data/usePassword.txt | 1 - src/handlers/api.ts | 142 - src/handlers/auth.ts | 72 - src/handlers/conf.ts | 98 - src/handlers/data.ts | 123 - src/handlers/frontend.ts | 138 - src/handlers/graph.ts | 82 - src/handlers/ha.ts | 70 - src/handlers/notification.ts | 76 - src/handlers/response.ts | 41 - src/handlers/stack.ts | 168 - src/index.ts | 45 + src/init.ts | 69 - src/middleware/authMiddleware.ts | 51 - src/middleware/checkLock.ts | 21 - src/middleware/rateLimiter.ts | 8 - src/misc/.tmux.sh | 1 - src/misc/createEnvDev.sh | 44 - src/misc/createEnvFile.sh | 44 - src/misc/credits.sh | 29 - .../dependencyGraphs/.dependency-cruiser.cjs | 359 - .../dependencyGraphs/createDependencyGraph.sh | 41 - src/misc/dependencyGraphs/mermaid-all.txt | 113 - src/misc/dependencyGraphs/mermaid-api.txt | 26 - src/misc/dependencyGraphs/mermaid-auth.txt | 19 - src/misc/dependencyGraphs/mermaid-conf.txt | 26 - src/misc/dependencyGraphs/mermaid-data.txt | 19 - .../dependencyGraphs/mermaid-frontend.txt | 19 - src/misc/dependencyGraphs/mermaid-graph.txt | 15 - src/misc/dependencyGraphs/mermaid-ha.txt | 31 - .../mermaid-notificationService.txt | 15 - src/misc/entrypoint.sh | 35 - src/misc/minifyDist.sh | 38 - src/misc/removeUnusedDeps.sh | 36 - src/plugins/example.plugin.ts | 11 + src/routes/auth/routes.ts | 18 - src/routes/container-logs.ts | 11 + src/routes/data/routes.ts | 20 - src/routes/docker.ts | 22 + src/routes/frontendController/routes.ts | 76 - src/routes/getter/routes.ts | 46 - src/routes/graphs/routes.ts | 20 - src/routes/highavailability/routes.ts | 27 - src/routes/logs.ts | 30 + src/routes/notifications/routes.ts | 20 - src/routes/setter/routes.ts | 20 - src/routes/stack/routes.ts | 35 - src/sample-variable.json | 24 - src/server.ts | 18 - src/typings/atomicWrite.ts | 6 - src/typings/dockerCompose.ts | 92 - src/typings/dockerConfig.ts | 35 - src/typings/dockerStackEnv.ts | 10 - src/typings/frontendConfig.ts | 12 - src/typings/ha.ts | 20 - src/typings/hostData.ts | 26 - src/typings/response.ts | 6 - src/typings/stackConfig.ts | 5 - src/typings/states.ts | 10 - src/typings/syncRequestBody.ts | 5 - src/typings/table.ts | 11 - src/typings/template.ts | 5 - src/utils/assets/api-icon.svg | 1 - src/utils/assets/container-icon.svg | 1 - src/utils/assets/server-icon.svg | 1 - src/utils/atomicWrite.ts | 35 - src/utils/connectionChecker.ts | 67 - src/utils/containerService.ts | 173 - src/utils/dockerClient.ts | 41 - src/utils/extractHostData.ts | 73 - src/utils/logger.ts | 79 - src/utils/notifications/_notify.ts | 51 - src/utils/notifications/_template.ts | 76 - src/utils/notifications/discord.ts | 56 - src/utils/notifications/email.ts | 53 - src/utils/notifications/pushbullet.ts | 59 - src/utils/notifications/pushover.ts | 57 - src/utils/notifications/slack.ts | 56 - src/utils/notifications/telegram.ts | 56 - src/utils/notifications/whatsapp.ts | 58 - src/utils/rateLimitFS.ts | 36 - src/utils/startServer.ts | 16 - src/utils/swaggerDocs.ts | 12 - src/utils/webSocket.ts | 113 - tsconfig.json | 118 +- 139 files changed, 625 insertions(+), 22275 deletions(-) delete mode 100644 .dockerignore delete mode 100644 .github/DockStat-dark.png delete mode 100644 .github/DockStat.png delete mode 100644 .github/workflows/build-image.yaml delete mode 100644 .github/workflows/cloc.yaml delete mode 100644 .github/workflows/remove-stale.yaml delete mode 100644 .github/workflows/validation.yaml delete mode 100644 .npmrc delete mode 100644 .nvmrc delete mode 100644 CREDITS.md delete mode 100644 LICENSE delete mode 100644 TODO.md delete mode 100644 __tests__/auth.spec.ts delete mode 100644 __tests__/config.spec.ts delete mode 100644 __tests__/database.spec.ts delete mode 100644 __tests__/frontend.spec.ts delete mode 100644 __tests__/getters.spec.ts delete mode 100644 __tests__/util/previousResponse.ts create mode 100644 bun.lock delete mode 100644 docker/Dockerfile-base delete mode 100644 docker/Dockerfile-dev delete mode 100644 docker/docker-compose.dev.yaml delete mode 100644 docker/docker-compose.yaml delete mode 100644 environment.d.ts delete mode 100644 eslint.config.mjs delete mode 100644 nodemon.json delete mode 100644 package-lock.json delete mode 100644 src/config/db.ts delete mode 100644 src/config/hostsystem.ts delete mode 100644 src/config/initFiles.ts delete mode 100644 src/config/stacks.ts delete mode 100644 src/config/swagger.yaml delete mode 100644 src/config/swaggerConfig.ts delete mode 100644 src/config/swaggerTheme.ts delete mode 100644 src/config/variables.ts delete mode 100644 src/controllers/auth.ts delete mode 100644 src/controllers/containerController.ts delete mode 100644 src/controllers/databaseMigration.ts delete mode 100644 src/controllers/fetchData.ts delete mode 100644 src/controllers/frontendConfiguration.ts delete mode 100644 src/controllers/highAvailability.ts delete mode 100644 src/controllers/notificationController.ts delete mode 100644 src/controllers/proxy.ts delete mode 100644 src/controllers/scheduler.ts create mode 100644 src/core/database/repository.ts create mode 100644 src/core/docker/host-manager.ts create mode 100644 src/core/plugins/loader.ts create mode 100644 src/core/plugins/plugin-manager.ts create mode 100644 src/core/utils/logger.ts delete mode 100644 src/data/frontendConfiguration.json delete mode 100644 src/data/template.json delete mode 100644 src/data/usePassword.txt delete mode 100644 src/handlers/api.ts delete mode 100644 src/handlers/auth.ts delete mode 100644 src/handlers/conf.ts delete mode 100644 src/handlers/data.ts delete mode 100644 src/handlers/frontend.ts delete mode 100644 src/handlers/graph.ts delete mode 100644 src/handlers/ha.ts delete mode 100644 src/handlers/notification.ts delete mode 100644 src/handlers/response.ts delete mode 100644 src/handlers/stack.ts create mode 100644 src/index.ts delete mode 100644 src/init.ts delete mode 100644 src/middleware/authMiddleware.ts delete mode 100644 src/middleware/checkLock.ts delete mode 100644 src/middleware/rateLimiter.ts delete mode 100644 src/misc/.tmux.sh delete mode 100755 src/misc/createEnvDev.sh delete mode 100755 src/misc/createEnvFile.sh delete mode 100755 src/misc/credits.sh delete mode 100644 src/misc/dependencyGraphs/.dependency-cruiser.cjs delete mode 100755 src/misc/dependencyGraphs/createDependencyGraph.sh delete mode 100644 src/misc/dependencyGraphs/mermaid-all.txt delete mode 100644 src/misc/dependencyGraphs/mermaid-api.txt delete mode 100644 src/misc/dependencyGraphs/mermaid-auth.txt delete mode 100644 src/misc/dependencyGraphs/mermaid-conf.txt delete mode 100644 src/misc/dependencyGraphs/mermaid-data.txt delete mode 100644 src/misc/dependencyGraphs/mermaid-frontend.txt delete mode 100644 src/misc/dependencyGraphs/mermaid-graph.txt delete mode 100644 src/misc/dependencyGraphs/mermaid-ha.txt delete mode 100644 src/misc/dependencyGraphs/mermaid-notificationService.txt delete mode 100755 src/misc/entrypoint.sh delete mode 100755 src/misc/minifyDist.sh delete mode 100755 src/misc/removeUnusedDeps.sh create mode 100644 src/plugins/example.plugin.ts delete mode 100644 src/routes/auth/routes.ts create mode 100644 src/routes/container-logs.ts delete mode 100644 src/routes/data/routes.ts create mode 100644 src/routes/docker.ts delete mode 100644 src/routes/frontendController/routes.ts delete mode 100644 src/routes/getter/routes.ts delete mode 100644 src/routes/graphs/routes.ts delete mode 100644 src/routes/highavailability/routes.ts create mode 100644 src/routes/logs.ts delete mode 100644 src/routes/notifications/routes.ts delete mode 100644 src/routes/setter/routes.ts delete mode 100644 src/routes/stack/routes.ts delete mode 100644 src/sample-variable.json delete mode 100644 src/server.ts delete mode 100644 src/typings/atomicWrite.ts delete mode 100644 src/typings/dockerCompose.ts delete mode 100644 src/typings/dockerConfig.ts delete mode 100644 src/typings/dockerStackEnv.ts delete mode 100644 src/typings/frontendConfig.ts delete mode 100644 src/typings/ha.ts delete mode 100644 src/typings/hostData.ts delete mode 100644 src/typings/response.ts delete mode 100644 src/typings/stackConfig.ts delete mode 100644 src/typings/states.ts delete mode 100644 src/typings/syncRequestBody.ts delete mode 100644 src/typings/table.ts delete mode 100644 src/typings/template.ts delete mode 100644 src/utils/assets/api-icon.svg delete mode 100644 src/utils/assets/container-icon.svg delete mode 100644 src/utils/assets/server-icon.svg delete mode 100644 src/utils/atomicWrite.ts delete mode 100644 src/utils/connectionChecker.ts delete mode 100644 src/utils/containerService.ts delete mode 100644 src/utils/dockerClient.ts delete mode 100644 src/utils/extractHostData.ts delete mode 100644 src/utils/logger.ts delete mode 100644 src/utils/notifications/_notify.ts delete mode 100644 src/utils/notifications/_template.ts delete mode 100644 src/utils/notifications/discord.ts delete mode 100644 src/utils/notifications/email.ts delete mode 100644 src/utils/notifications/pushbullet.ts delete mode 100644 src/utils/notifications/pushover.ts delete mode 100644 src/utils/notifications/slack.ts delete mode 100644 src/utils/notifications/telegram.ts delete mode 100644 src/utils/notifications/whatsapp.ts delete mode 100644 src/utils/rateLimitFS.ts delete mode 100644 src/utils/startServer.ts delete mode 100644 src/utils/swaggerDocs.ts delete mode 100644 src/utils/webSocket.ts diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 6381947..0000000 --- a/.dockerignore +++ /dev/null @@ -1,151 +0,0 @@ -# custom paths: -src/data/* -.tmp -docker/master -docker/slave -.test* -# Created by https://www.toptal.com/developers/gitignore/api/node -### Node ### -*-audit.json -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* -.pnpm-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# Snowpack dependency directory (https://snowpack.dev/) -web_modules/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional stylelint cache -.stylelintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variable files -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# parcel-bundler cache (https://parceljs.org/) -.cache -.parcel-cache - -# Next.js build output -.next -out - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# vuepress v2.x temp and cache directory -.temp - -# Docusaurus cache and generated files -.docusaurus - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test - -# yarn v2 -.yarn/cache -.yarn/unplugged -.yarn/build-state.yml -.yarn/install-state.gz -.pnp.* - -### Node Patch ### -# Serverless Webpack directories -.webpack/ - -# Optional stylelint cache - -# SvelteKit build / generate output -.svelte-kit -/test-results/ -/playwright-report/ -/blob-report/ -/playwright/.cache/ diff --git a/.github/DockStat-dark.png b/.github/DockStat-dark.png deleted file mode 100644 index 00ac779a6dcb303ce5c690aa079dedc175e5c8f3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 82847 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

TLgl9u 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 AX#fBK diff --git a/.github/DockStat.png b/.github/DockStat.png deleted file mode 100644 index d375bd49107c79a960488d6062276a72cf6bd512..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 79885 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⪼EggaS+_T%ej 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 KU+70|um2a&GMD85 diff --git a/.github/workflows/build-image.yaml b/.github/workflows/build-image.yaml deleted file mode 100644 index 9d43ff1..0000000 --- a/.github/workflows/build-image.yaml +++ /dev/null @@ -1,63 +0,0 @@ -name: "Build and Push Docker Image" - -on: - release: - types: [published] - -permissions: - packages: write - contents: read - -jobs: - build-release: - runs-on: ubuntu-24.04 - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ github.token }} - - - name: Extract version and create tag - id: get-tag - run: | - # Remove 'v' prefix from release tag if present - VERSION="${GITHUB_REF#refs/tags/v}" - # Check if pre-release and append '-pre' - if ${{ github.event.release.prerelease }}; then - TAG="$VERSION-pre" - else - TAG="$VERSION" - fi - echo "tag=$TAG" >> $GITHUB_OUTPUT - - - name: Generate Docker metadata - uses: docker/metadata-action@v5 - id: metadata - with: - images: ghcr.io/${{ github.repository }} - tags: | - type=raw,value=${{ steps.get-tag.outputs.tag }} - type=raw,value=latest,enable=${{ !github.event.release.prerelease }} - - - name: Build and push - uses: docker/build-push-action@v6 - with: - platforms: linux/amd64,linux/arm64 - context: . - file: docker/Dockerfile-base - push: true - tags: ${{ steps.metadata.outputs.tags }} - labels: ${{ steps.metadata.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max diff --git a/.github/workflows/cloc.yaml b/.github/workflows/cloc.yaml deleted file mode 100644 index 0f4245f..0000000 --- a/.github/workflows/cloc.yaml +++ /dev/null @@ -1,27 +0,0 @@ -name: "Count Lines of Code" - -permissions: - issues: write - pull-requests: write - -on: - pull_request: - branches: [main, dev] - -jobs: - cloc: - runs-on: ubuntu-24.04 - - steps: - - uses: actions/checkout@v4 - - - name: Count Lines of Code (cloc) - uses: djdefi/cloc-action@6 - with: - options: --md --report-file=cloc.md --exclude-dir=node_modules --exclude-lang=YAML,JSON --exclude-list-file=package-lock.json - - - name: Create comment from markdown file - uses: GrantBirki/comment@v2 - with: - file: cloc.md - issue-number: ${{ github.event.number }} diff --git a/.github/workflows/remove-stale.yaml b/.github/workflows/remove-stale.yaml deleted file mode 100644 index 47f9ae2..0000000 --- a/.github/workflows/remove-stale.yaml +++ /dev/null @@ -1,17 +0,0 @@ -name: "Close stale issues and PR" -on: - schedule: - - cron: "30 1 * * *" - -jobs: - remove-stale: - runs-on: ubuntu-24.04 - steps: - - uses: actions/stale@v9 - with: - stale-issue-message: "This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days." - stale-pr-message: "This PR is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 10 days." - close-issue-message: "This issue was closed because it has been stalled for 5 days with no activity." - days-before-stale: 30 - days-before-close: 5 - days-before-pr-close: -1 diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml deleted file mode 100644 index 9a9ec93..0000000 --- a/.github/workflows/validation.yaml +++ /dev/null @@ -1,211 +0,0 @@ -name: "CI/CD Pipeline" - -on: - push: - release: - types: [published] - -jobs: - validation: - name: "Code Validation & Tests" - runs-on: ubuntu-24.04 - permissions: - actions: read - contents: read - packages: read - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Node.js 20 - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: npm - - - name: Install dependencies - run: npm ci - - - name: Create varaibles.json - run: npm run local-env-file - - - name: Run code formatting - run: npm run prettier - - - name: Run linter - run: npm run lint - - - name: Build project - run: npm run build:mini - - - name: Audit packages - run: npm audit --audit-level=high - - - name: Run tests - run: npm run test:silent - - security-analysis: - name: "Security Analysis" - runs-on: ubuntu-24.04 - needs: validation - permissions: - security-events: write - contents: read - packages: read - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: javascript-typescript - queries: security-extended - config: | - query-filter: - - exclude: - tags: /cwe-200/ - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - - container-scanning: - name: "Container Security" - runs-on: ubuntu-24.04 - needs: validation - permissions: - security-events: write - contents: read - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Download Grype - run: | - curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b $HOME/bin - echo "$HOME/bin" >> $GITHUB_PATH - - - name: Build Docker image - run: docker build . --file docker/Dockerfile-base --tag localbuild/testimage:latest - - - name: Run vulnerability scan - run: grype -o sarif localbuild/testimage:latest > results.sarif - - - name: Upload SARIF report - uses: github/codeql-action/upload-sarif@v3 - with: - sarif_file: ./results.sarif - - build-test: - name: "Docker Build Test" - runs-on: ubuntu-24.04 - needs: validation - permissions: - contents: read - packages: read - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Build Docker image - uses: docker/build-push-action@v6 - with: - context: . - file: docker/Dockerfile-base - platforms: linux/amd64,linux/arm64 - push: false - cache-from: type=gha - cache-to: type=gha,mode=max - - todo-management: - name: "TODO Issue Management" - runs-on: ubuntu-24.04 - needs: validation - permissions: - contents: write - issues: write - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Process TODOs - uses: alstr/todo-to-issue-action@v5 - with: - INSERT_ISSUE_URLS: "true" - - - name: Commit changes - run: | - git config --global user.name "github-actions[bot]" - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git add -A - if [[ $(git status --porcelain) ]]; then - git commit -m "Automatically process TODOs [skip ci]" - git push - fi - - deployment: - name: "Docker Deployment" - runs-on: ubuntu-24.04 - needs: [security-analysis, container-scanning, build-test] - permissions: - packages: write - contents: read - strategy: - matrix: - include: - - type: dev - # Only enable when pushing to the dev branch - enabled: ${{ github.ref_name == 'dev' }} - - type: pre-release - # Only enable when a release event is published and it's a prerelease - enabled: ${{ github.event_name == 'release' && github.event.release.prerelease }} - - type: release - # Only enable when a release event is published and it's NOT a prerelease - enabled: ${{ github.event_name == 'release' && !github.event.release.prerelease }} - steps: - - name: Exit early if deployment is not enabled - if: ${{ !matrix.enabled }} - run: | - echo "Skipping deployment for matrix type '${{ matrix.type }}' because conditions are not met." - exit 0 - - - name: Checkout repository - if: ${{ matrix.enabled }} - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - if: ${{ matrix.enabled }} - uses: docker/setup-buildx-action@v3 - - - name: Login to GitHub Container Registry - if: ${{ matrix.enabled }} - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Determine tags - if: ${{ matrix.enabled }} - id: tags - uses: docker/metadata-action@v5 - with: - images: ghcr.io/${{ github.repository }} - tags: | - type=raw,value=${{ matrix.type == 'dev' && 'nightly' || matrix.type == 'pre-release' && 'pre' || matrix.type == 'release' && 'latest' }} - - - name: Build and push - if: ${{ matrix.enabled }} - uses: docker/build-push-action@v6 - with: - context: . - file: docker/Dockerfile-dev - platforms: linux/amd64,linux/arm64 - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.tags.outputs.tags }} - labels: ${{ steps.tags.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max diff --git a/.gitignore b/.gitignore index 9e264ac..4bc7b0a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,154 +1,44 @@ -# custom paths: -src/data/* -src/data/frontendConfiguration.json +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. -.tmp -docker/master -docker/slave -.test* -stacks -# Created by https://www.toptal.com/developers/gitignore/api/node -### Node ### -*-audit.json -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* -.pnpm-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt +*.db -# Bower dependency directory (https://bower.io/) -bower_components +# dependencies +/node_modules +/.pnp +.pnp.js -# node-waf configuration -.lock-wscript +# testing +/coverage -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release +# next.js +/.next/ +/out/ -# Dependency directories -node_modules/ -jspm_packages/ +# production +/build -# Snowpack dependency directory (https://snowpack.dev/) -web_modules/ +# misc +.DS_Store +*.pem -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional stylelint cache -.stylelintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* -# dotenv environment variable files -.env +# local env files +.env.local .env.development.local .env.test.local .env.production.local -.env.local - -# parcel-bundler cache (https://parceljs.org/) -.cache -.parcel-cache - -# Next.js build output -.next -out - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# vuepress v2.x temp and cache directory -.temp - -# Docusaurus cache and generated files -.docusaurus - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test - -# yarn v2 -.yarn/cache -.yarn/unplugged -.yarn/build-state.yml -.yarn/install-state.gz -.pnp.* - -### Node Patch ### -# Serverless Webpack directories -.webpack/ -# Optional stylelint cache +# vercel +.vercel -# SvelteKit build / generate output -.svelte-kit -/test-results/ -/playwright-report/ -/blob-report/ -/playwright/.cache/ +**/*.trace +**/*.zip +**/*.tar.gz +**/*.tgz +**/*.log +package-lock.json +**/*.bun diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 4fd0219..0000000 --- a/.npmrc +++ /dev/null @@ -1 +0,0 @@ -engine-strict=true \ No newline at end of file diff --git a/.nvmrc b/.nvmrc deleted file mode 100644 index 209e3ef..0000000 --- a/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -20 diff --git a/CREDITS.md b/CREDITS.md deleted file mode 100644 index 6dd2d89..0000000 --- a/CREDITS.md +++ /dev/null @@ -1,106 +0,0 @@ -# CREDITS - -This file shows all npm packages used in DockStatAPI (also Dev packages) - -### License: (MIT AND CC-BY-3.0) - -| Name | Repository | Publisher | -| ----------------- | -------------------------------------------- | -------------------- | -| spdx-ranges@2.1.1 | https://github.com/kemitchell/spdx-ranges.js | The Linux Foundation | - -### License: Apache 2.0 - -| Name | Repository | Publisher | -| ---------------------- | ------------------------------------------ | --------- | -| qrcode-terminal@0.12.0 | https://github.com/gtanner/qrcode-terminal | N/A | - -### License: Apache-2.0 - -| Name | Repository | Publisher | -| ------------------------------------ | ------------------------------------------------------------------------ | -------------------- | -| @ampproject/remapping@2.3.0 | https://github.com/ampproject/remapping | Justin Ridgewell | -| @balena/dockerignore@1.0.2 | https://github.com/balena-io-modules/dockerignore | N/A | -| @eslint/config-array@0.19.2 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/core@0.10.0 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/core@0.11.0 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/object-schema@2.1.6 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/plugin-kit@0.2.5 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @grpc/grpc-js@1.12.6 | https://github.com/grpc/grpc-node/tree/master/packages/grpc-js | Google Inc. | -| @grpc/proto-loader@0.7.13 | https://github.com/grpc/grpc-node | Google Inc. | -| @humanfs/core@0.19.1 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | -| @humanfs/node@0.16.6 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | -| @humanwhocodes/module-importer@1.0.1 | https://github.com/humanwhocodes/module-importer | Nicholas C. Zaks | -| @humanwhocodes/retry@0.3.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | -| @humanwhocodes/retry@0.4.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | -| @puppeteer/browsers@2.7.1 | https://github.com/puppeteer/puppeteer/tree/main/packages/browsers | The Chromium Authors | -| @scarf/scarf@1.4.0 | https://github.com/scarf-sh/scarf-js | Scarf Systems | -| @sigstore/bundle@3.0.0 | https://github.com/sigstore/sigstore-js | bdehamer@github.com | -| @sigstore/core@2.0.0 | https://github.com/sigstore/sigstore-js | bdehamer@github.com | -| @sigstore/protobuf-specs@0.3.3 | https://github.com/sigstore/protobuf-specs | bdehamer@github.com | -| @sigstore/sign@3.0.0 | https://github.com/sigstore/sigstore-js | bdehamer@github.com | -| @sigstore/tuf@3.0.0 | https://github.com/sigstore/sigstore-js | bdehamer@github.com | -| @sigstore/verify@2.0.0 | https://github.com/sigstore/sigstore-js | bdehamer@github.com | -| b4a@1.6.7 | https://github.com/holepunchto/b4a | Holepunch | -| bare-events@2.5.4 | https://github.com/holepunchto/bare-events | Holepunch | -| bare-fs@4.0.1 | https://github.com/holepunchto/bare-fs | Holepunch | -| bare-os@3.4.0 | https://github.com/holepunchto/bare-os | Holepunch | -| bare-path@3.0.0 | https://github.com/holepunchto/bare-path | Holepunch | -| bare-stream@2.6.5 | https://github.com/holepunchto/bare-stream | Holepunch | -| bser@2.1.1 | https://github.com/facebook/watchman | Wez Furlong | -| chromium-bidi@1.2.0 | https://github.com/GoogleChromeLabs/chromium-bidi | The Chromium Authors | -| detect-libc@2.0.3 | https://github.com/lovell/detect-libc | Lovell Fuller | -| docker-modem@5.0.6 | https://github.com/apocas/docker-modem | Pedro Dias | -| dockerode@4.0.4 | https://github.com/apocas/dockerode | Pedro Dias | -| ejs@3.1.10 | https://github.com/mde/ejs | Matthew Eernisse | -| eslint-visitor-keys@3.4.3 | https://github.com/eslint/eslint-visitor-keys | Toru Nagashima | -| eslint-visitor-keys@4.2.0 | https://github.com/eslint/js | Toru Nagashima | -| exponential-backoff@3.1.1 | https://github.com/coveo/exponential-backoff | Sami Sayegh | -| fb-watchman@2.0.2 | https://github.com/facebook/watchman | Wez Furlong | -| filelist@1.0.4 | https://github.com/mde/filelist | Matthew Eernisse | -| human-signals@2.1.0 | https://github.com/ehmicky/human-signals | ehmicky | -| jake@10.9.2 | https://github.com/jakejs/jake | Matthew Eernisse | -| long@5.2.4 | https://github.com/dcodeIO/long.js | Daniel Wirtz | -| puppeteer-core@24.2.0 | https://github.com/puppeteer/puppeteer/tree/main/packages/puppeteer-core | The Chromium Authors | -| puppeteer@24.2.0 | https://github.com/puppeteer/puppeteer/tree/main/packages/puppeteer | The Chromium Authors | -| sigstore@3.0.0 | https://github.com/sigstore/sigstore-js | bdehamer@github.com | -| spdx-correct@3.2.0 | https://github.com/jslicense/spdx-correct.js | N/A | -| swagger-ui-dist@5.18.3 | https://github.com/swagger-api/swagger-ui | N/A | -| text-decoder@1.2.3 | https://github.com/holepunchto/text-decoder | Holepunch | -| tunnel-agent@0.6.0 | https://github.com/mikeal/tunnel-agent | Mikeal Rogers | -| typescript@5.7.3 | https://github.com/microsoft/TypeScript | Microsoft Corp. | -| validate-npm-package-license@3.0.4 | https://github.com/kemitchell/validate-npm-package-license.js | Kyle E. Mitchell | -| walker@1.0.8 | https://github.com/daaku/nodejs-walker | Naitik Shah | - -### License: Artistic-2.0 - -| Name | Repository | Publisher | -| ---------- | -------------------------- | ----------- | -| npm@11.1.0 | https://github.com/npm/cli | GitHub Inc. | - -### License: BlueOak-1.0.0 - -| Name | Repository | Publisher | -| ---------------------------- | ------------------------------------------------ | ------------------ | -| chownr@3.0.0 | https://github.com/isaacs/chownr | Isaac Z. Schlueter | -| jackspeak@3.4.3 | https://github.com/isaacs/jackspeak | Isaac Z. Schlueter | -| package-json-from-dist@1.0.1 | https://github.com/isaacs/package-json-from-dist | Isaac Z. Schlueter | -| path-scurry@1.11.1 | https://github.com/isaacs/path-scurry | Isaac Z. Schlueter | -| yallist@5.0.0 | https://github.com/isaacs/yallist | Isaac Z. Schlueter | - -### License: CC-BY-3.0 - -| Name | Repository | Publisher | -| --------------------- | -------------------------------------------------- | -------------------- | -| spdx-exceptions@2.5.0 | https://github.com/kemitchell/spdx-exceptions.json | The Linux Foundation | - -### License: CC-BY-4.0 - -| Name | Repository | Publisher | -| ------------------------- | -------------------------------------------- | ---------- | -| caniuse-lite@1.0.30001698 | https://github.com/browserslist/caniuse-lite | Ben Briggs | - -### License: Python-2.0 - -| Name | Repository | Publisher | -| -------------- | ---------------------------------- | --------- | -| argparse@2.0.1 | https://github.com/nodeca/argparse | N/A | diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 1e9eceb..0000000 --- a/LICENSE +++ /dev/null @@ -1,28 +0,0 @@ -BSD 3-Clause License - -Copyright (c) 2024, ItsNik - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index 2577866..6cc99af 100644 --- a/README.md +++ b/README.md @@ -1,71 +1,3 @@ -# DockStatAPI v2 +# REWRITE -![Dockstat Logo](.github/DockStat.png) - -

- -# Pipelines - -[![Docker Image CI](https://img.shields.io/github/actions/workflow/status/Its4Nik/dockstatapi/build-image.yml?branch=main&label=Docker%20Image%20CI&style=for-the-badge&logo=docker)](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-image.yml) -[![Validation](https://img.shields.io/github/actions/workflow/status/Its4Nik/dockstatapi/validation.yml?branch=dev&label=Validation&style=for-the-badge&logo=checkmarx)](https://github.com/Its4Nik/dockstatapi/actions/workflows/validation.yml) - -
- -This specific branch contains the currently WIP **DockStatAPI-v2**, this update will bring major breaking changes so please be careful. -With this new release a couple of extra features (compared to v1) are going to be available. - -### Feature List: - -- Swagger API Documentation -- Database (Keeps data for 24 hours max) -- Advanced authentication using hashes and salt -- Custom TypeScript/JavaScript notification modules! (Easy to add and configure!) -- `http` API to configure the backend -- Multi-arch docker builds (using buildx github action) -- Advanced security through middlewares: rate-limiting and authentication -- Multi Arch Docker builds through docker buildx -- High Availability using single master and unlimited worker nodes! -- Dynamically created Graphs - -# 🔗 DockStatAPI v2 Documentation - -_⚠️ = Deprecation warning_ - -- [Introduction](https://outline.itsnik.de/s/dockstat) - - - [DockstatAPI v2](https://outline.itsnik.de/s/dockstat/doc/dockstatapi-v2-XRMDKRqMIg) - - - [API reference](https://outline.itsnik.de/s/dockstat/doc/api-reference-1PTxqx1MQ6) - - [How dependency graphs are made](https://outline.itsnik.de/s/dockstat/doc/how-the-dependecy-graphs-are-made-svuZbEHH9g) - - - [DockStat v1](https://outline.itsnik.de/s/dockstat/doc/dockstat-v1-zVaFS4zROI) - - - [⚠️ Customisation](https://outline.itsnik.de/s/dockstat/doc/customization-PiBz4OpQIZ) - - [⚠️ Themes](https://outline.itsnik.de/s/dockstat/doc/themes-BFhN6ZBbYx) - - [⚠️ Installation](https://outline.itsnik.de/s/dockstat/doc/installation-DaO99bB86q) - - - [⚠️ DockStatAPI v1](https://outline.itsnik.de/s/dockstat/doc/dockstatapi-v1-jLcVCfPNmS) - - [⚠️ Integrations](https://outline.itsnik.de/s/dockstat/doc/integrations-Agq1oL6HxF) - - [⚠️ Backend API reference](https://outline.itsnik.de/s/dockstat/doc/backend-api-reference-YzcBbDvY33) - -# Dependencies - -Please see [CREDITS.md](./CREDITS.md). - -To create the credits file use: `npm run license` - -Or if you want it as a pre-commit hook create this file: - -```bash -#!/bin/bash -# .git/hooks/pre-commit - -npm run license -``` - -# DockStat(APIs) goals - -DockStack tries to be a lightweigh and more "dashboard" like then [portainer](https://github.com/portainer/portainer), [cAdvisor](https://github.com/google/cadvisor), [dockge](https://github.com/louislam/dockge), ... -I also try to add some "extensions", like in V1 with [🥤cup](https://github.com/sergi0g/cup). -Everything is configured through a backend with Swagger documentation, so that you can follow the code and understand the new v2 frontend better! -DockStat is mainly used for teaching [myself](https://github.com/Its4Nik) more about TypeScript, APIs and backend development! +Using Bun, keep an eye out! diff --git a/TODO.md b/TODO.md deleted file mode 100644 index b850ba7..0000000 --- a/TODO.md +++ /dev/null @@ -1,18 +0,0 @@ -- [x] ~Better Offline mode using "faker" library or self written (probably self written)~ Not needed since there is a docker-compsoe file for local testing integrated inside the repo -- [x] HA compatibility -- [x] !!! Needs testing !!! Add automatic notifications when container state changes, according to selected level for notification service -- [ ] Image update and update notifications -- [ ] trigger container restart / stop / start via backend routes -- [x] Add more logging -- [x] Structure code differently -- [x] Write new README and make the docs better -- [x] Update more files to correct TS syntax => remove "any" -- [x] Websockets -- [x] Better /api/status endpoint with connection status of each host -- [x] Update notification service -- [x] Adjust process.env variables since they don't really work as expected (See [commit](https://github.com/Its4Nik/dockstatapi/pull/21/commits/a03b58c7a17e269f46216df5492e18d008774961)) -- [ ] Better project structure -- [x] Update logging => Better errors -- [x] Update json responses -- [x] Swagger update -- [ ] Edge case testing diff --git a/__tests__/auth.spec.ts b/__tests__/auth.spec.ts deleted file mode 100644 index 84c5f04..0000000 --- a/__tests__/auth.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -export const testPass = "123456789"; -import { Server } from "http"; -import supertest from "supertest"; -import { startServer } from "../src/utils/startServer"; -import app from "../src/server"; - -const port = 13001; -const server = new Server(app); - -startServer(app, server, port); - -const request = supertest(`http://localhost:${port}`); - -describe("Authentication", () => { - it("Enable Authentication", async () => { - const res = await request.post(`/auth/enable?password=${testPass}`); - expect(res.status).toEqual(200); - expect(res.type).toEqual(expect.stringContaining("json")); - expect(res.body).toHaveProperty( - "message", - "Authentication enabled successfully", - ); - }); - - it("Test no password", async () => { - const res = await request.get("/api/status"); - expect(res.status).toEqual(403); - expect(res.type).toEqual(expect.stringContaining("json")); - }); - - it("Disable authentication", async () => { - const res = await request - .post(`/auth/disable?password=${testPass}`) - .set("x-password", testPass); - expect(res.status).toEqual(200); - expect(res.type).toEqual(expect.stringContaining("json")); - }); -}); diff --git a/__tests__/config.spec.ts b/__tests__/config.spec.ts deleted file mode 100644 index 2650e9e..0000000 --- a/__tests__/config.spec.ts +++ /dev/null @@ -1,49 +0,0 @@ -import supertest from "supertest"; -import { startServer } from "../src/utils/startServer"; -import app from "../src/server"; -import { Server } from "http"; - -const port = 13002; -const server = new Server(app); - -startServer(app, server, port); - -const request = supertest(`http://localhost:${port}`); - -const mockServerName: string = "mockstatapi"; -const mockServerIP: string = "127.0.0.1"; -const mockServerPort: number = 2375; - -describe("Config endpoints", () => { - it("Add an host", async () => { - let res = await request.put( - `/conf/addHost?name=${mockServerName}&url=${mockServerIP}&port=${mockServerPort}`, - ); - expect(res.status).toEqual(200); - - res = await request.get("/api/hosts"); - expect(res.status).toEqual(200); - expect(res.body).toContain("mockstatapi"); - }); - - it("Adjust scheduler", async () => { - let res = await request.put("/conf/scheduler?interval=10m"); - expect(res.status).toEqual(200); - - res = await request.get("/api/current-schedule"); - expect(res.status).toEqual(200); - - // Reset to standart 5m - res = await request.put("/conf/scheduler?interval=5m"); - expect(res.status).toEqual(200); - }); - - it("Remove Host from config", async () => { - let res = await request.delete(`/conf/removeHost?hostName=mockstatapi`); - expect(res.status).toEqual(200); - - res = await request.get("/api/hosts"); - expect(res.status).toEqual(200); - expect(res.body).not.toHaveProperty("mockstatapi"); - }); -}); diff --git a/__tests__/database.spec.ts b/__tests__/database.spec.ts deleted file mode 100644 index 55102ce..0000000 --- a/__tests__/database.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import supertest from "supertest"; -import { startServer } from "../src/utils/startServer"; -import app from "../src/server"; -import { Server } from "http"; - -const port = 13003; -const server = new Server(app); - -startServer(app, server, port); - -const request = supertest(`http://localhost:${port}`); - -describe("Database", () => { - it("Get latest database entry", async () => { - const res = await request.get("/data/latest"); - expect(res.status).toEqual(200); - }); - - it("Get all database entries", async () => { - const res = await request.get("/data/all"); - expect(res.status).toEqual(200); - }); - - it("Clear database", async () => { - let res = await request.delete("/data/clear"); - expect(res.status).toEqual(200); - - res = await request.get("/data/latest"); - expect(res.status).toEqual(404); - expect(res.body).toHaveProperty( - "message", - "No data available for /data/latest", - ); - }); -}); diff --git a/__tests__/frontend.spec.ts b/__tests__/frontend.spec.ts deleted file mode 100644 index af25adc..0000000 --- a/__tests__/frontend.spec.ts +++ /dev/null @@ -1,123 +0,0 @@ -import supertest from "supertest"; -import { startServer } from "../src/utils/startServer"; -import app from "../src/server"; -import { Server } from "http"; - -const port = 13004; -const server = new Server(app); - -startServer(app, server, port); - -const request = supertest(`http://localhost:${port}`); - -const sec: number = 1000; - -const mockContainer: string = "dockstatapi"; -const mockLink: string = "https://github.com/its4nik/dockstatapi"; -const mockIcon: string = "dockstatapi.png"; -const mockTag1: string = "backend"; -const mockTag2: string = "local"; - -const verifiedResponse = [ - { - name: "dockstatapi", - tags: ["backend", "local"], - pinned: true, - link: "https://github.com/its4nik/dockstatapi", - icon: "dockstatapi.png", - hidden: true, - }, -]; - -describe("Test frontend specific configurations", () => { - it( - "Setup the configuration file", - async () => { - // Hide container - let res = await request.delete(`/frontend/hide/${mockContainer}`); - - expect(res.status).toEqual(200); - - // Add Tag(s) - res = await request.post(`/frontend/tag/${mockContainer}/${mockTag1}`); - - expect(res.status).toEqual(200); - res = await request.post(`/frontend/tag/${mockContainer}/${mockTag2}`); - - expect(res.status).toEqual(200); - - // Pin container - res = await request.post(`/frontend/pin/${mockContainer}`); - - expect(res.status).toEqual(200); - - // Add link - res = await request.post( - `/frontend/add-link/${mockContainer}/${encodeURIComponent(mockLink)}`, - ); - - expect(res.status).toEqual(200); - - // Add icon - res = await request.post( - `/frontend/add-icon/${mockContainer}/${mockIcon}/false`, - ); - - expect(res.status).toEqual(200); - }, - 60 * sec, - ); - - it("Verify the configuration", async () => { - const res = await request.get("/api/frontend-config"); - - expect(res.status).toEqual(200); - expect(res.body).toEqual(verifiedResponse); - }); - - it( - "Reset configuration", - async () => { - // Show container - let res = await request.post(`/frontend/show/${mockContainer}`); - - expect(res.status).toEqual(200); - - // Remove tag(s) - res = await request.delete( - `/frontend/remove-tag/${mockContainer}/${mockTag1}`, - ); - - expect(res.status).toEqual(200); - - res = await request.delete( - `/frontend/remove-tag/${mockContainer}/${mockTag2}`, - ); - - expect(res.status).toEqual(200); - - // Unpin - res = await request.delete(`/frontend/unpin/${mockContainer}`); - - expect(res.status).toEqual(200); - - // Remove link - res = await request.delete(`/frontend/remove-link/${mockContainer}`); - - expect(res.status).toEqual(200); - - // Remove icon - res = await request.delete(`/frontend/remove-icon/${mockContainer}`); - - expect(res.status).toEqual(200); - }, - 60 * sec, - ); - - it("Verify the reset configuration", async () => { - const res = await request.get("/api/frontend-config"); - - expect(res.status).toEqual(200); - expect(res.body).toEqual([]); - }); -}); diff --git a/__tests__/getters.spec.ts b/__tests__/getters.spec.ts deleted file mode 100644 index f951f42..0000000 --- a/__tests__/getters.spec.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { createPreviousResponse } from "./util/previousResponse"; -import supertest from "supertest"; -import { startServer } from "../src/utils/startServer"; -import app from "../src/server"; -import { Server } from "http"; - -const port = 13005; -const server = new Server(app); - -startServer(app, server, port); - -const request = supertest(`http://localhost:${port}`); -const PreviousResponse = createPreviousResponse(); - -describe("Get endpoints", () => { - it("GET /api/hosts", async () => { - const res = await request.get("/api/hosts"); - expect(res.status).toEqual(200); - expect(res.type).toEqual(expect.stringContaining("json")); - - const hosts: string[] = res.body; - - if (hosts.length >= 1) { - expect(Array.isArray(hosts)).toBe(true); - expect(hosts.length).toBeGreaterThan(0); - expect(typeof hosts[0]).toBe("string"); - PreviousResponse.set(hosts[0]); - } - }); - - it("GET /api/host/:host/stats", async () => { - const host = PreviousResponse.get(); - - if (!host) { - console.log("No hosts found, skipping /api/host/:host/stats test"); - return; - } - - const res = await request.get(`/api/host/${host}/stats`); - - expect(res.status).toEqual(200); - expect(res.type).toEqual(expect.stringContaining("json")); - }); - - it("GET /api/system", async () => { - const res = await request.get("/api/system"); - expect(res.status).toEqual(200); - expect(res.type).toEqual(expect.stringContaining("json")); - }); - - it("GET /api/status", async () => { - const res = await request.get("/api/status"); - expect(res.status).toEqual(200); - expect(res.type).toEqual(expect.stringContaining("json")); - expect(res.body).toHaveProperty("ApiReachable", true); - }); - - it("GET /api/containers", async () => { - const res = await request.get("/api/containers"); - expect(res.status).toEqual(200); - expect(res.type).toEqual(expect.stringContaining("json")); - }); - - it("GET /api/config", async () => { - const res = await request.get("/api/config"); - expect(res.status).toEqual(200); - expect(res.type).toEqual(expect.stringContaining("json")); - expect(res.body).toHaveProperty("hosts"); - }); - - it("GET /api/current-schedule", async () => { - const res = await request.get("/api/current-schedule"); - - expect(res.status).toEqual(200); - expect(res.type).toEqual(expect.stringContaining("json")); - expect(res.body).toHaveProperty("interval"); - }); - - it("GET /api/frontend-config", async () => { - const res = await request.get("/api/frontend-config"); - - expect(res.status).toEqual(200); - expect(res.type).toEqual(expect.stringContaining("json")); - }); - - it("GET /ha/config", async () => { - const res = await request.get("/ha/config"); - expect(res.status).toEqual(200); - expect(res.type).toEqual(expect.stringContaining("json")); - }); - - it("GET /notification-service/get-template", async () => { - const res = await request.get("/notification-service/get-template"); - - expect(res.status).toEqual(200); - expect(res.type).toEqual(expect.stringContaining("json")); - expect(res.body).toHaveProperty("text"); - }); -}); diff --git a/__tests__/util/previousResponse.ts b/__tests__/util/previousResponse.ts deleted file mode 100644 index 774a862..0000000 --- a/__tests__/util/previousResponse.ts +++ /dev/null @@ -1,23 +0,0 @@ -let response: string = ""; - -class PreviousResponse { - set(body: unknown): void { - try { - response = JSON.stringify(body).replace(/[" ]/g, ""); - } catch (error: unknown) { - console.error("Error in setting response:", error); - throw new Error("Failed to set response"); - } - } - - get(): string { - try { - return response; - } catch (error: unknown) { - console.error("Error in getting response:", error); - throw new Error("Failed to get response"); - } - } -} - -export const createPreviousResponse = () => new PreviousResponse(); diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..a5ee6c8 --- /dev/null +++ b/bun.lock @@ -0,0 +1,119 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "dockstatapi", + "dependencies": { + "@elysiajs/swagger": "^1.2.2", + "chalk": "^5.4.1", + "elysia": "latest", + "winston": "^3.17.0", + "winston-transport": "^4.9.0", + }, + "devDependencies": { + "bun-types": "latest", + }, + }, + }, + "packages": { + "@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="], + + "@dabh/diagnostics": ["@dabh/diagnostics@2.0.3", "", { "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA=="], + + "@elysiajs/swagger": ["@elysiajs/swagger@1.2.2", "", { "dependencies": { "@scalar/themes": "^0.9.52", "@scalar/types": "^0.0.12", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.2.0" } }, "sha512-DG0PbX/wzQNQ6kIpFFPCvmkkWTIbNWDS7lVLv3Puy6ONklF14B4NnbDfpYjX1hdSYKeCqKBBOuenh6jKm8tbYA=="], + + "@scalar/openapi-types": ["@scalar/openapi-types@0.1.1", "", {}, "sha512-NMy3QNk6ytcCoPUGJH0t4NNr36OWXgZhA3ormr3TvhX1NDgoF95wFyodGVH8xiHeUyn2/FxtETm8UBLbB5xEmg=="], + + "@scalar/themes": ["@scalar/themes@0.9.68", "", { "dependencies": { "@scalar/types": "0.0.34" } }, "sha512-466ac2fdQJOBBSLkGUf88vuZVF+qNMeVpjb0aAHrKkxhpjucTPKdTYO8r2dsX1R5k9A13gWPnm594VW5G/bGHw=="], + + "@scalar/types": ["@scalar/types@0.0.12", "", { "dependencies": { "@scalar/openapi-types": "0.1.1", "@unhead/schema": "^1.9.5" } }, "sha512-XYZ36lSEx87i4gDqopQlGCOkdIITHHEvgkuJFrXFATQs9zHARop0PN0g4RZYWj+ZpCUclOcaOjbCt8JGe22mnQ=="], + + "@sinclair/typebox": ["@sinclair/typebox@0.34.27", "", {}, "sha512-C7mxE1VC3WC2McOufZXEU48IfRVI+BcKxk4NOyNn3+JMUNdJHEWGS5CqjuDX+ij2NCCz8/nse1mT7yn8Fv2GHg=="], + + "@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], + + "@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="], + + "@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="], + + "@unhead/schema": ["@unhead/schema@1.11.19", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-7VhYHWK7xHgljdv+C01MepCSYZO2v6OhgsfKWPxRQBDDGfUKCUaChox0XMq3tFvXP6u4zSp6yzcDw2yxCfVMwg=="], + + "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], + + "bun-types": ["bun-types@1.2.3", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-P7AeyTseLKAvgaZqQrvp3RqFM3yN9PlcLuSTe7SoJOfZkER73mLdT2vEQi8U64S1YvM/ldcNiQjn0Sn7H9lGgg=="], + + "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], + + "color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="], + + "color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + + "color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + + "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], + + "colorspace": ["colorspace@1.1.4", "", { "dependencies": { "color": "^3.1.3", "text-hex": "1.0.x" } }, "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w=="], + + "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], + + "elysia": ["elysia@1.2.21", "", { "dependencies": { "@sinclair/typebox": "^0.34.27", "cookie": "^1.0.2", "memoirist": "^0.3.0", "openapi-types": "^12.1.3" }, "peerDependencies": { "typescript": ">= 5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-E9b1JcB7fiQ2ptk24W8OnBrMYUoKzffIXob9uTVUKhqOKxaXAd9UyWBeyr7JCDa/VD/b/9S8aIey9/YJsK5sLg=="], + + "enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="], + + "fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="], + + "fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="], + + "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], + + "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="], + + "logform": ["logform@2.7.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="], + + "memoirist": ["memoirist@0.3.0", "", {}, "sha512-wR+4chMgVPq+T6OOsk40u9Wlpw1Pjx66NMNiYxCQQ4EUJ7jDs3D9kTCeKdBOkvAiqXlHLVJlvYL01PvIJ1MPNg=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="], + + "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], + + "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], + + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], + + "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], + + "stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="], + + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="], + + "triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="], + + "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "winston": ["winston@3.17.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw=="], + + "winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="], + + "zhead": ["zhead@2.2.4", "", {}, "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag=="], + + "@scalar/themes/@scalar/types": ["@scalar/types@0.0.34", "", { "dependencies": { "@scalar/openapi-types": "0.1.8", "@unhead/schema": "^1.11.11" } }, "sha512-q01ctijmHArM5KOny2zU+sHfhpsgOAENrDENecK2TsQNn5FYLmFZouMKeW2M6F7KFLPZnFxUiL/rT88b6Rp/Kg=="], + + "@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.1.8", "", {}, "sha512-iufA5/6hPCmRIVD2eh7qGpoKvoA08Gw/qUb2JECifBtAwA93fo7+1k9uHK440f2LMJsbxIzA+nv7RS0BmfiO/g=="], + } +} diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base deleted file mode 100644 index f21146b..0000000 --- a/docker/Dockerfile-base +++ /dev/null @@ -1,76 +0,0 @@ -# Stage 1: Build stage -FROM node:20-alpine AS builder - -LABEL maintainer="https://github.com/its4nik" -LABEL version="2.0.1" -LABEL description="API for DockStat" -LABEL license="BSD-3-Clause license" -LABEL repository="https://github.com/its4nik/dockstatapi" -LABEL documentation="https://github.com/its4nik/dockstatapi" -LABEL org.opencontainers.image.description="The DockSatAPI is a free and OpenSource backend for gathering container statistics across hosts" -LABEL org.opencontainers.image.licenses="BSD-3-Clause license" -LABEL org.opencontainers.image.source="https://github.com/its4nik/dockstatapi" - -WORKDIR /app - -ENV NODE_NO_WARNINGS=1 - -RUN apk add --no-cache curl bash - -COPY package*.json tsconfig.json environment.d.ts ./ - -RUN npm ci --include=dev - -COPY ./src ./src -RUN mv ./src/sample-variable.json ./src/data/variables.json - -RUN npm run build:mini - -# -------------------------------------- -# Stage 2: Dependency pruning stage -FROM node:20-alpine AS deps -WORKDIR /api -COPY --from=builder /app/package*.json . -RUN npm ci --omit=dev - -# -------------------------------------- -# Stage 3: Final production image -FROM node:20-alpine AS prod - -WORKDIR /api - -RUN apk add --no-cache docker-cli bash curl && \ - mkdir -p /usr/libexec/docker/cli-plugins && \ - curl -sSL "https://github.com/docker/compose/releases/latest/download/docker-compose-linux-$(uname -m)" \ - -o /usr/libexec/docker/cli-plugins/docker-compose && \ - chmod +x /usr/libexec/docker/cli-plugins/docker-compose && \ - rm -rf /var/cache/apk/* - -ARG USER_ID=10001 -ARG GROUP_ID=10001 -RUN addgroup -g $GROUP_ID dockstatapi && \ - adduser -u $USER_ID -G dockstatapi -h /api -s /bin/sh -D dockstatapi - -COPY --from=builder --chown=dockstatapi:dockstatapi /app/dist/src ./src -COPY --from=builder --chown=dockstatapi:dockstatapi /app/src/config/swagger.yaml ./src/config/swagger.yaml -COPY --from=builder --chown=dockstatapi:dockstatapi /app/src/utils/assets ./src/utils/assets -COPY --from=builder /app/package.json ./ -COPY --from=deps --chown=dockstatapi:dockstatapi /api/node_modules ./node_modules - -COPY --from=builder --chown=dockstatapi:dockstatapi /app/src/misc/entrypoint.sh . -COPY --from=builder --chown=dockstatapi:dockstatapi /app/src/misc/createEnvFile.sh . -RUN chmod +x *.sh - -RUN mkdir -p /api/src/data && \ - chown -R dockstatapi:dockstatapi /api && \ - chmod -R 755 /api && \ - chmod 775 /api/src/data - -HEALTHCHECK --interval=5m --timeout=3s \ - CMD curl -f http://localhost:9876/api/status || exit 1 - -EXPOSE 9876 -STOPSIGNAL 130 -USER dockstatapi - -ENTRYPOINT [ "sh", "./entrypoint.sh", "--prod" ] diff --git a/docker/Dockerfile-dev b/docker/Dockerfile-dev deleted file mode 100644 index 00b8800..0000000 --- a/docker/Dockerfile-dev +++ /dev/null @@ -1,76 +0,0 @@ -# Stage 1: Build stage -FROM node:20-alpine AS builder - -LABEL maintainer="https://github.com/its4nik" -LABEL version="2.0.1" -LABEL description="API for DockStat" -LABEL license="BSD-3-Clause license" -LABEL repository="https://github.com/its4nik/dockstatapi" -LABEL documentation="https://github.com/its4nik/dockstatapi" -LABEL org.opencontainers.image.description="The DockSatAPI is a free and OpenSource backend for gathering container statistics across hosts" -LABEL org.opencontainers.image.licenses="BSD-3-Clause license" -LABEL org.opencontainers.image.source="https://github.com/its4nik/dockstatapi" - -WORKDIR /app - -ENV NODE_NO_WARNINGS=1 - -RUN apk add --no-cache curl bash - -COPY package*.json tsconfig.json environment.d.ts ./ - -RUN npm ci --include=dev - -COPY ./src ./src -RUN mv ./src/sample-variable.json ./src/data/variables.json - -RUN npm run build - -# -------------------------------------- -# Stage 2: Dependency pruning stage -FROM node:20-alpine AS deps -WORKDIR /api -COPY --from=builder /app/package*.json . -RUN npm ci --omit=dev - -# -------------------------------------- -# Stage 3: Final production image -FROM node:20-alpine AS prod - -WORKDIR /api - -RUN apk add --no-cache docker-cli bash curl && \ - mkdir -p /usr/libexec/docker/cli-plugins && \ - curl -sSL "https://github.com/docker/compose/releases/latest/download/docker-compose-linux-$(uname -m)" \ - -o /usr/libexec/docker/cli-plugins/docker-compose && \ - chmod +x /usr/libexec/docker/cli-plugins/docker-compose && \ - rm -rf /var/cache/apk/* - -ARG USER_ID=10001 -ARG GROUP_ID=10001 -RUN addgroup -g $GROUP_ID dockstatapi && \ - adduser -u $USER_ID -G dockstatapi -h /api -s /bin/sh -D dockstatapi - -COPY --from=builder --chown=dockstatapi:dockstatapi /app/dist/src ./src -COPY --from=builder --chown=dockstatapi:dockstatapi /app/src/config/swagger.yaml ./src/config/swagger.yaml -COPY --from=builder --chown=dockstatapi:dockstatapi /app/src/utils/assets ./src/utils/assets -COPY --from=builder /app/package.json ./ -COPY --from=deps --chown=dockstatapi:dockstatapi /api/node_modules ./node_modules - -COPY --from=builder --chown=dockstatapi:dockstatapi /app/src/misc/entrypoint.sh . -COPY --from=builder --chown=dockstatapi:dockstatapi /app/src/misc/createEnvFile.sh . -RUN chmod +x *.sh - -RUN mkdir -p /api/src/data && \ - chown -R dockstatapi:dockstatapi /api && \ - chmod -R 755 /api && \ - chmod 775 /api/src/data - -HEALTHCHECK --interval=5m --timeout=3s \ - CMD curl -f http://localhost:9876/api/status || exit 1 - -EXPOSE 9876 -STOPSIGNAL 130 -USER dockstatapi - -ENTRYPOINT [ "sh", "./entrypoint.sh", "--dev" ] diff --git a/docker/docker-compose.dev.yaml b/docker/docker-compose.dev.yaml deleted file mode 100644 index 7bc3773..0000000 --- a/docker/docker-compose.dev.yaml +++ /dev/null @@ -1,40 +0,0 @@ -services: - test-socket-proxy: - image: lscr.io/linuxserver/socket-proxy:latest - container_name: test-socket-proxy - environment: - - ALLOW_START=1 #optional - - ALLOW_STOP=1 #optional - - ALLOW_RESTARTS=1 #optional - - AUTH=0 #optional - - BUILD=0 #optional - - COMMIT=0 #optional - - CONFIGS=0 #optional - - CONTAINERS=1 #optional - - DISABLE_IPV6=0 #optional - - DISTRIBUTION=0 #optional - - EVENTS=1 #optional - - EXEC=0 #optional - - IMAGES=0 #optional - - INFO=1 #optional - - NETWORKS=1 #optional - - NODES=1 #optional - - PING=1 #optional - - POST=0 #optional - - PLUGINS=0 #optional - - SECRETS=0 #optional - - SERVICES=0 #optional - - SESSION=0 #optional - - SWARM=0 #optional - - SYSTEM=0 #optional - - TASKS=0 #optional - - VERSION=1 #optional - - VOLUMES=0 #optional - volumes: - - /var/run/docker.sock:/var/run/docker.sock:ro - restart: unless-stopped - read_only: true - tmpfs: - - /run - ports: - - 2375:2375 \ No newline at end of file diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml deleted file mode 100644 index 436d8a2..0000000 --- a/docker/docker-compose.yaml +++ /dev/null @@ -1,82 +0,0 @@ -networks: - shared-network: - driver: bridge - -services: - master: - container_name: master - user: "${UID:-1000}:${GID:-1000}" - environment: - - NODE_ENV=development - - HA_MASTER=true - - HA_MASTER_IP=master:9876 - - HA_NODE=slave:9876 - - HA_UNSAFE=true - volumes: - - ./master/data:/api/src/data - - ./master/logs:/api/logs - ports: - - 9876:9876 - image: dockstatapi:local - networks: - - shared-network - depends_on: - - slave - - test-socket-proxy - - slave: - container_name: slave - user: "${UID:-1000}:${GID:-1000}" - environment: - - NODE_ENV=development - volumes: - - ./slave/data:/api/src/data - - ./slave/logs:/api/logs - ports: - - 6789:9876 - image: dockstatapi:local - depends_on: - - test-socket-proxy - networks: - - shared-network - - test-socket-proxy: - image: lscr.io/linuxserver/socket-proxy:latest - container_name: test-socket-proxy - environment: - - ALLOW_START=1 #optional - - ALLOW_STOP=1 #optional - - ALLOW_RESTARTS=1 #optional - - AUTH=0 #optional - - BUILD=0 #optional - - COMMIT=0 #optional - - CONFIGS=0 #optional - - CONTAINERS=1 #optional - - DISABLE_IPV6=0 #optional - - DISTRIBUTION=0 #optional - - EVENTS=1 #optional - - EXEC=0 #optional - - IMAGES=0 #optional - - INFO=1 #optional - - NETWORKS=1 #optional - - NODES=1 #optional - - PING=1 #optional - - POST=0 #optional - - PLUGINS=0 #optional - - SECRETS=0 #optional - - SERVICES=0 #optional - - SESSION=0 #optional - - SWARM=0 #optional - - SYSTEM=0 #optional - - TASKS=0 #optional - - VERSION=1 #optional - - VOLUMES=0 #optional - volumes: - - /var/run/docker.sock:/var/run/docker.sock:ro - restart: unless-stopped - read_only: true - tmpfs: - - /run - networks: - - shared-network - diff --git a/environment.d.ts b/environment.d.ts deleted file mode 100644 index df2595f..0000000 --- a/environment.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -declare global { - namespace NodeJS { - interface ProcessEnv { - // Node specific: - NODE_ENV: "development" | "production" | "testing"; - PORT: string | undefined; - CI: "true" | null; - } - } -} - -export {}; diff --git a/eslint.config.mjs b/eslint.config.mjs deleted file mode 100644 index 56994a6..0000000 --- a/eslint.config.mjs +++ /dev/null @@ -1,12 +0,0 @@ -import globals from "globals"; -import pluginJs from "@eslint/js"; -import tseslint from "typescript-eslint"; - -/** @type {import('eslint').Linter.Config[]} */ -export default [ - { ignores: ["node_modules/*", "dist/*"] }, - { files: ["src/**/*.ts"] }, - { languageOptions: { globals: globals.node } }, - pluginJs.configs.recommended, - ...tseslint.configs.recommended, -]; diff --git a/nodemon.json b/nodemon.json deleted file mode 100644 index be32c75..0000000 --- a/nodemon.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "ignore": [ - "**/data/**", - "src/logs", - "**/fixtures/**", - ".gitignore", - "**/*.json", - "**/__tests__/**" - ], - "execMap": { - "ts": "tsx" - }, - "delay": 2500 -} diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 6efc7ed..0000000 --- a/package-lock.json +++ /dev/null @@ -1,13317 +0,0 @@ -{ - "name": "dockstatapi", - "version": "2.0.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "dockstatapi", - "version": "2.0.1", - "license": "BSD 3-Clause License", - "dependencies": { - "bcrypt": "^5.1.1", - "chokidar": "^4.0.1", - "cors": "^2.8.5", - "cytoscape": "^3.30.4", - "docker-compose": "^1.1.0", - "dockerode": "^4.0.2", - "express": "^4.21.1", - "express-rate-limit": "^7.4.1", - "https": "^1.0.0", - "i": "^0.3.7", - "ipaddr.js": "^2.2.0", - "nodemailer": "^6.9.16", - "npm": "^11.0.0", - "puppeteer": "^24.0.0", - "sqlite3": "^5.1.7", - "swagger-ui-express": "^5.0.1", - "winston": "^3.15.0", - "winston-daily-rotate-file": "^5.0.0", - "yamljs": "^0.3.0" - }, - "devDependencies": { - "@eslint/js": "^9.17.0", - "@types/bcrypt": "^5.0.2", - "@types/cors": "^2.8.17", - "@types/cytoscape": "^3.21.8", - "@types/dockerode": "^3.3.31", - "@types/express": "^5.0.0", - "@types/express-handlebars": "^5.3.1", - "@types/jest": "^29.5.14", - "@types/node": "^22.9.0", - "@types/node-fetch": "^2.6.12", - "@types/nodemailer": "^6.4.17", - "@types/supertest": "^6.0.2", - "@types/supports-color": "^8.1.3", - "@types/swagger-jsdoc": "^6.0.4", - "@types/swagger-ui-express": "^4.1.7", - "@types/ws": "^8.5.14", - "@types/yamljs": "^0.2.34", - "@typescript-eslint/eslint-plugin": "^8.18.2", - "@typescript-eslint/parser": "^8.18.2", - "dependency-cruiser": "^16.5.0", - "eslint": "^9.17.0", - "globals": "^15.14.0", - "jest": "^29.7.0", - "license-checker": "^25.0.1", - "nodemon": "^3.1.7", - "prettier": "^3.4.2", - "supertest": "^7.0.0", - "ts-jest": "^29.2.5", - "ts-node": "^10.9.2", - "tsx": "^4.19.2", - "typescript-eslint": "^8.18.2", - "uglify-js": "^3.19.3" - }, - "engines": { - "npm": ">=10.8.2" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.5.tgz", - "integrity": "sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.26.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.7.tgz", - "integrity": "sha512-SRijHmF0PSPgLIBYlWnG0hyeJLwXE2CgpsXaMOrtt2yp9/86ALw6oUlj9KYuZ0JN07T4eBMVIW4li/9S1j2BGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.5", - "@babel/helper-compilation-targets": "^7.26.5", - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.7", - "@babel/parser": "^7.26.7", - "@babel/template": "^7.25.9", - "@babel/traverse": "^7.26.7", - "@babel/types": "^7.26.7", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz", - "integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.26.5", - "@babel/types": "^7.26.5", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", - "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.26.5", - "@babel/helper-validator-option": "^7.25.9", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", - "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", - "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", - "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", - "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.26.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.7.tgz", - "integrity": "sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.26.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.7.tgz", - "integrity": "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.26.7" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", - "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", - "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", - "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", - "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.26.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.7.tgz", - "integrity": "sha512-1x1sgeyRLC3r5fQOM0/xtQKsYjyxmFjaOrLJNtZ81inNjyJHGIolTULPiSc/2qe1/qfpFLisLQYFnnZl7QoedA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.5", - "@babel/parser": "^7.26.7", - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.7", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/types": { - "version": "7.26.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.7.tgz", - "integrity": "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@balena/dockerignore": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz", - "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==", - "license": "Apache-2.0" - }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@colors/colors": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", - "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", - "license": "MIT", - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "node_modules/@dabh/diagnostics": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", - "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", - "license": "MIT", - "dependencies": { - "colorspace": "1.1.x", - "enabled": "2.0.x", - "kuler": "^2.0.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", - "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", - "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", - "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", - "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", - "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", - "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", - "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", - "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", - "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", - "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", - "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", - "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", - "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", - "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", - "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", - "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", - "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", - "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", - "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", - "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", - "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", - "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", - "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", - "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", - "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", - "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.6", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/core": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.11.0.tgz", - "integrity": "sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", - "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/js": { - "version": "9.20.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.20.0.tgz", - "integrity": "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz", - "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.10.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", - "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@gar/promisify": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", - "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", - "license": "MIT", - "optional": true - }, - "node_modules/@grpc/grpc-js": { - "version": "1.12.6", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.12.6.tgz", - "integrity": "sha512-JXUj6PI0oqqzTGvKtzOkxtpsyPRNsrmhh41TtIz/zEB6J+AUiZZ0dxWzcMwO9Ns5rmSPuMdghlTbUuqIM48d3Q==", - "license": "Apache-2.0", - "dependencies": { - "@grpc/proto-loader": "^0.7.13", - "@js-sdsl/ordered-map": "^4.4.2" - }, - "engines": { - "node": ">=12.10.0" - } - }, - "node_modules/@grpc/proto-loader": { - "version": "0.7.13", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", - "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", - "license": "Apache-2.0", - "dependencies": { - "lodash.camelcase": "^4.3.0", - "long": "^5.0.0", - "protobufjs": "^7.2.5", - "yargs": "^17.7.2" - }, - "bin": { - "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", - "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/console": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", - "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/core": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", - "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/reporters": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-changed-files": "^29.7.0", - "jest-config": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-resolve-dependencies": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "jest-watcher": "^29.7.0", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/environment": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", - "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "expect": "^29.7.0", - "jest-snapshot": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/expect-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", - "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-get-type": "^29.6.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/fake-timers": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", - "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@sinonjs/fake-timers": "^10.0.2", - "@types/node": "*", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/globals": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", - "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/types": "^29.6.3", - "jest-mock": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/reporters": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", - "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "@types/node": "*", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^6.0.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "slash": "^3.0.0", - "string-length": "^4.0.1", - "strip-ansi": "^6.0.0", - "v8-to-istanbul": "^9.0.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/source-map": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", - "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.18", - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/test-result": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", - "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/test-sequencer": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", - "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/transform": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", - "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@js-sdsl/ordered-map": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", - "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/js-sdsl" - } - }, - "node_modules/@mapbox/node-pre-gyp": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", - "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", - "license": "BSD-3-Clause", - "dependencies": { - "detect-libc": "^2.0.0", - "https-proxy-agent": "^5.0.0", - "make-dir": "^3.1.0", - "node-fetch": "^2.6.7", - "nopt": "^5.0.0", - "npmlog": "^5.0.1", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.11" - }, - "bin": { - "node-pre-gyp": "bin/node-pre-gyp" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@npmcli/fs": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", - "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", - "license": "ISC", - "optional": true, - "dependencies": { - "@gar/promisify": "^1.0.1", - "semver": "^7.3.5" - } - }, - "node_modules/@npmcli/move-file": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", - "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", - "deprecated": "This functionality has been moved to @npmcli/fs", - "license": "MIT", - "optional": true, - "dependencies": { - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@npmcli/move-file/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "license": "MIT", - "optional": true, - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", - "license": "BSD-3-Clause" - }, - "node_modules/@puppeteer/browsers": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.7.1.tgz", - "integrity": "sha512-MK7rtm8JjaxPN7Mf1JdZIZKPD2Z+W7osvrC1vjpvfOX1K0awDIHYbNi89f7eotp7eMUn2shWnt03HwVbriXtKQ==", - "license": "Apache-2.0", - "dependencies": { - "debug": "^4.4.0", - "extract-zip": "^2.0.1", - "progress": "^2.0.3", - "proxy-agent": "^6.5.0", - "semver": "^7.7.0", - "tar-fs": "^3.0.8", - "yargs": "^17.7.2" - }, - "bin": { - "browsers": "lib/cjs/main-cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@puppeteer/browsers/node_modules/tar-fs": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.8.tgz", - "integrity": "sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg==", - "license": "MIT", - "dependencies": { - "pump": "^3.0.0", - "tar-stream": "^3.1.5" - }, - "optionalDependencies": { - "bare-fs": "^4.0.1", - "bare-path": "^3.0.0" - } - }, - "node_modules/@puppeteer/browsers/node_modules/tar-stream": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", - "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", - "license": "MIT", - "dependencies": { - "b4a": "^1.6.4", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, - "node_modules/@scarf/scarf": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", - "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", - "hasInstallScript": true, - "license": "Apache-2.0" - }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", - "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.0" - } - }, - "node_modules/@tootallnate/once": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "license": "MIT" - }, - "node_modules/@tsconfig/node10": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", - "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", - "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.20.6", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", - "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.20.7" - } - }, - "node_modules/@types/bcrypt": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", - "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/body-parser": { - "version": "1.19.5", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", - "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/cookiejar": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", - "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/cors": { - "version": "2.8.17", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", - "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/cytoscape": { - "version": "3.21.9", - "resolved": "https://registry.npmjs.org/@types/cytoscape/-/cytoscape-3.21.9.tgz", - "integrity": "sha512-JyrG4tllI6jvuISPjHK9j2Xv/LTbnLekLke5otGStjFluIyA9JjgnvgZrSBsp8cEDpiTjwgZUZwpPv8TSBcoLw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/docker-modem": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz", - "integrity": "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/ssh2": "*" - } - }, - "node_modules/@types/dockerode": { - "version": "3.3.34", - "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.34.tgz", - "integrity": "sha512-mH9SuIb8NuTDsMus5epcbTzSbEo52fKLBMo0zapzYIAIyfDqoIFn7L3trekHLKC8qmxGV++pPUP4YqQ9n5v2Zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/docker-modem": "*", - "@types/node": "*", - "@types/ssh2": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/express": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", - "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^5.0.0", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-handlebars": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/@types/express-handlebars/-/express-handlebars-5.3.1.tgz", - "integrity": "sha512-DSzaERLO4gHb8AqnrL58jzSDyT0yDdl6HqDc+bGz1Hf0nrG1FK30nHGzv8NBEGR8QV9eUGB/YaE0Qj3NjF7siw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/express-serve-static-core": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", - "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/graceful-fs": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", - "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/http-errors": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, - "node_modules/@types/jest": { - "version": "29.5.14", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", - "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "expect": "^29.0.0", - "pretty-format": "^29.0.0" - } - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/methods": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", - "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "22.13.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.1.tgz", - "integrity": "sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==", - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "node_modules/@types/node-fetch": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", - "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "form-data": "^4.0.0" - } - }, - "node_modules/@types/nodemailer": { - "version": "6.4.17", - "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz", - "integrity": "sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/qs": { - "version": "6.9.18", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", - "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/send": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", - "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.7", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", - "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "*" - } - }, - "node_modules/@types/ssh2": { - "version": "1.15.4", - "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.4.tgz", - "integrity": "sha512-9JTQgVBWSgq6mAen6PVnrAmty1lqgCMvpfN+1Ck5WRUsyMYPa6qd50/vMJ0y1zkGpOEgLzm8m8Dx/Y5vRouLaA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "^18.11.18" - } - }, - "node_modules/@types/ssh2/node_modules/@types/node": { - "version": "18.19.75", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.75.tgz", - "integrity": "sha512-UIksWtThob6ZVSyxcOqCLOUNg/dyO1Qvx4McgeuhrEtHTLFTf7BBhEazaE4K806FGTPtzd/2sE90qn4fVr7cyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/@types/ssh2/node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/superagent": { - "version": "8.1.9", - "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", - "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/cookiejar": "^2.1.5", - "@types/methods": "^1.1.4", - "@types/node": "*", - "form-data": "^4.0.0" - } - }, - "node_modules/@types/supertest": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.2.tgz", - "integrity": "sha512-137ypx2lk/wTQbW6An6safu9hXmajAifU/s7szAHLN/FeIm5w7yR0Wkl9fdJMRSHwOn4HLAI0DaB2TOORuhPDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/methods": "^1.1.4", - "@types/superagent": "^8.1.0" - } - }, - "node_modules/@types/supports-color": { - "version": "8.1.3", - "resolved": "https://registry.npmjs.org/@types/supports-color/-/supports-color-8.1.3.tgz", - "integrity": "sha512-Hy6UMpxhE3j1tLpl27exp1XqHD7n8chAiNPzWfz16LPZoMMoSc4dzLl6w9qijkEb/r5O1ozdu1CWGA2L83ZeZg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/swagger-jsdoc": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.4.tgz", - "integrity": "sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/swagger-ui-express": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.7.tgz", - "integrity": "sha512-ovLM9dNincXkzH4YwyYpll75vhzPBlWx6La89wwvYH7mHjVpf0X0K/vR/aUM7SRxmr5tt9z7E5XJcjQ46q+S3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/express": "*", - "@types/serve-static": "*" - } - }, - "node_modules/@types/triple-beam": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", - "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", - "license": "MIT" - }, - "node_modules/@types/ws": { - "version": "8.5.14", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.14.tgz", - "integrity": "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/yamljs": { - "version": "0.2.34", - "resolved": "https://registry.npmjs.org/@types/yamljs/-/yamljs-0.2.34.tgz", - "integrity": "sha512-gJvfRlv9ErxdOv7ux7UsJVePtX54NAvQyd8ncoiFqK8G5aeHIfQfGH2fbruvjAQ9657HwAaO54waS+Dsk2QTUQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/yargs": { - "version": "17.0.33", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", - "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.23.0.tgz", - "integrity": "sha512-vBz65tJgRrA1Q5gWlRfvoH+w943dq9K1p1yDBY2pc+a1nbBLZp7fB9+Hk8DaALUbzjqlMfgaqlVPT1REJdkt/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.23.0", - "@typescript-eslint/type-utils": "8.23.0", - "@typescript-eslint/utils": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0", - "graphemer": "^1.4.0", - "ignore": "^5.3.1", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.0.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.23.0.tgz", - "integrity": "sha512-h2lUByouOXFAlMec2mILeELUbME5SZRN/7R9Cw2RD2lRQQY08MWMM+PmVVKKJNK1aIwqTo9t/0CvOxwPbRIE2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.23.0", - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/typescript-estree": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.23.0.tgz", - "integrity": "sha512-OGqo7+dXHqI7Hfm+WqkZjKjsiRtFUQHPdGMXzk5mYXhJUedO7e/Y7i8AK3MyLMgZR93TX4bIzYrfyVjLC+0VSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.23.0.tgz", - "integrity": "sha512-iIuLdYpQWZKbiH+RkCGc6iu+VwscP5rCtQ1lyQ7TYuKLrcZoeJVpcLiG8DliXVkUxirW/PWlmS+d6yD51L9jvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "8.23.0", - "@typescript-eslint/utils": "8.23.0", - "debug": "^4.3.4", - "ts-api-utils": "^2.0.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.23.0.tgz", - "integrity": "sha512-1sK4ILJbCmZOTt9k4vkoulT6/y5CHJ1qUYxqpF1K/DBAd8+ZUL4LlSCxOssuH5m4rUaaN0uS0HlVPvd45zjduQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.23.0.tgz", - "integrity": "sha512-LcqzfipsB8RTvH8FX24W4UUFk1bl+0yTOf9ZA08XngFwMg4Kj8A+9hwz8Cr/ZS4KwHrmo9PJiLZkOt49vPnuvQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.0.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <5.8.0" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.23.0.tgz", - "integrity": "sha512-uB/+PSo6Exu02b5ZEiVtmY6RVYO7YU5xqgzTIVZwTHvvK3HsL8tZZHFaTLFtRG3CsV4A5mhOv+NZx5BlhXPyIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.23.0", - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/typescript-estree": "8.23.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.23.0.tgz", - "integrity": "sha512-oWWhcWDLwDfu++BGTZcmXWqpwtkwb5o7fxUIGksMQQDSdPW9prsSnfIOZMlsj4vBOSrcnjIUZMiIjODgGosFhQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.23.0", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "license": "ISC" - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/acorn-jsx-walk": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/acorn-jsx-walk/-/acorn-jsx-walk-2.0.0.tgz", - "integrity": "sha512-uuo6iJj4D4ygkdzd6jPtcxs8vZgDX9YFIkqczGImoypX2fQ4dVImmu3UzA4ynixCIMTrEOWW+95M2HuBaCEOVA==", - "dev": true, - "license": "MIT" - }, - "node_modules/acorn-loose": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/acorn-loose/-/acorn-loose-8.4.0.tgz", - "integrity": "sha512-M0EUka6rb+QC4l9Z3T0nJEzNOO7JcoJlYMrBlyBCiFSXRyxjLKayd4TbQs2FDRWQU1h9FR7QVNHt+PEaoNL5rQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/agentkeepalive": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", - "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "humanize-ms": "^1.2.1" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "license": "MIT", - "optional": true, - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/aproba": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", - "license": "ISC" - }, - "node_modules/are-we-there-yet": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", - "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true, - "license": "MIT" - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, - "node_modules/array-find-index": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" - }, - "node_modules/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", - "dev": true, - "license": "MIT" - }, - "node_modules/asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", - "license": "MIT", - "dependencies": { - "safer-buffer": "~2.1.0" - } - }, - "node_modules/ast-types": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", - "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "license": "MIT" - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/b4a": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", - "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", - "license": "Apache-2.0" - }, - "node_modules/babel-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", - "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/transform": "^29.7.0", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.6.3", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.8.0" - } - }, - "node_modules/babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", - "test-exclude": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-istanbul/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/babel-plugin-jest-hoist": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", - "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.1.14", - "@types/babel__traverse": "^7.0.6" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/babel-preset-current-node-syntax": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", - "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-import-attributes": "^7.24.7", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/babel-preset-jest": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", - "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", - "dev": true, - "license": "MIT", - "dependencies": { - "babel-plugin-jest-hoist": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" - }, - "node_modules/bare-events": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz", - "integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==", - "license": "Apache-2.0", - "optional": true - }, - "node_modules/bare-fs": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.0.1.tgz", - "integrity": "sha512-ilQs4fm/l9eMfWY2dY0WCIUplSUp7U0CT1vrqMg1MUdeZl4fypu5UP0XcDBK5WBQPJAKP1b7XEodISmekH/CEg==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "bare-events": "^2.0.0", - "bare-path": "^3.0.0", - "bare-stream": "^2.0.0" - }, - "engines": { - "bare": ">=1.7.0" - } - }, - "node_modules/bare-os": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.4.0.tgz", - "integrity": "sha512-9Ous7UlnKbe3fMi7Y+qh0DwAup6A1JkYgPnjvMDNOlmnxNRQvQ/7Nst+OnUQKzk0iAT0m9BisbDVp9gCv8+ETA==", - "license": "Apache-2.0", - "optional": true, - "engines": { - "bare": ">=1.6.0" - } - }, - "node_modules/bare-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", - "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "bare-os": "^3.0.1" - } - }, - "node_modules/bare-stream": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz", - "integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "streamx": "^2.21.0" - }, - "peerDependencies": { - "bare-buffer": "*", - "bare-events": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - }, - "bare-events": { - "optional": true - } - } - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/basic-ftp": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", - "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/bcrypt": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", - "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@mapbox/node-pre-gyp": "^1.0.11", - "node-addon-api": "^5.0.0" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", - "license": "BSD-3-Clause", - "dependencies": { - "tweetnacl": "^0.14.3" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "license": "MIT", - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", - "license": "MIT", - "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.13.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/bs-logger": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", - "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-json-stable-stringify": "2.x" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "node-int64": "^0.4.0" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/buildcheck": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", - "integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==", - "optional": true, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/cacache": { - "version": "15.3.0", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", - "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", - "license": "ISC", - "optional": true, - "dependencies": { - "@npmcli/fs": "^1.0.0", - "@npmcli/move-file": "^1.0.1", - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "glob": "^7.1.4", - "infer-owner": "^1.0.4", - "lru-cache": "^6.0.0", - "minipass": "^3.1.1", - "minipass-collect": "^1.0.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.2", - "mkdirp": "^1.0.3", - "p-map": "^4.0.0", - "promise-inflight": "^1.0.1", - "rimraf": "^3.0.2", - "ssri": "^8.0.1", - "tar": "^6.0.2", - "unique-filename": "^1.1.1" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/cacache/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "license": "ISC", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/cacache/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "license": "MIT", - "optional": true, - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/cacache/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC", - "optional": true - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", - "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", - "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001698", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001698.tgz", - "integrity": "sha512-xJ3km2oiG/MbNU8G6zIq6XRZ6HtAOVXsbOrP/blGazi52kc5Yy7b6sDA5O+FbROzRrV7BSTllLHuNvmawYUJjw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/chromium-bidi": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-1.2.0.tgz", - "integrity": "sha512-XtdJ1GSN6S3l7tO7F77GhNsw0K367p0IsLYf2yZawCVAKKC3lUvDhPdMVrB2FNhmhfW43QGYbEX3Wg6q0maGwQ==", - "license": "Apache-2.0", - "dependencies": { - "mitt": "^3.0.1", - "zod": "^3.24.1" - }, - "peerDependencies": { - "devtools-protocol": "*" - } - }, - "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cjs-module-lexer": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", - "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" - } - }, - "node_modules/collect-v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", - "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/color": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", - "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.3", - "color-string": "^1.6.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "license": "MIT", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "node_modules/color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "license": "ISC", - "bin": { - "color-support": "bin.js" - } - }, - "node_modules/color/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "license": "MIT" - }, - "node_modules/colorspace": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", - "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", - "license": "MIT", - "dependencies": { - "color": "^3.1.3", - "text-hex": "1.0.x" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", - "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/component-emitter": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", - "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "license": "MIT" - }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "license": "ISC" - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "license": "MIT" - }, - "node_modules/cookiejar": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", - "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", - "dev": true, - "license": "MIT" - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/cosmiconfig": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", - "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/cpu-features": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", - "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", - "hasInstallScript": true, - "optional": true, - "dependencies": { - "buildcheck": "~0.0.6", - "nan": "^2.19.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/create-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", - "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "prompts": "^2.0.1" - }, - "bin": { - "create-jest": "bin/create-jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/cytoscape": { - "version": "3.31.0", - "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.31.0.tgz", - "integrity": "sha512-zDGn1K/tfZwEnoGOcHc0H4XazqAAXAuDpcYw9mUnUjATjqljyCNGJv8uEvbvxGaGHaVshxMecyl6oc6uKzRfbw==", - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", - "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/debuglog": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz", - "integrity": "sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/dedent": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", - "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "babel-plugin-macros": "^3.1.0" - }, - "peerDependenciesMeta": { - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/degenerator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", - "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", - "license": "MIT", - "dependencies": { - "ast-types": "^0.13.4", - "escodegen": "^2.1.0", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "license": "MIT" - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/dependency-cruiser": { - "version": "16.9.0", - "resolved": "https://registry.npmjs.org/dependency-cruiser/-/dependency-cruiser-16.9.0.tgz", - "integrity": "sha512-Gc/xHNOBq1nk5i7FPCuexCD0m2OXB/WEfiSHfNYQaQaHZiZltnl5Ixp/ZG38Jvi8aEhKBQTHV4Aw6gmR7rWlOw==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.14.0", - "acorn-jsx": "^5.3.2", - "acorn-jsx-walk": "^2.0.0", - "acorn-loose": "^8.4.0", - "acorn-walk": "^8.3.4", - "ajv": "^8.17.1", - "commander": "^13.0.0", - "enhanced-resolve": "^5.18.0", - "ignore": "^7.0.0", - "interpret": "^3.1.1", - "is-installed-globally": "^1.0.0", - "json5": "^2.2.3", - "memoize": "^10.0.0", - "picocolors": "^1.1.1", - "picomatch": "^4.0.2", - "prompts": "^2.4.2", - "rechoir": "^0.8.0", - "safe-regex": "^2.1.1", - "semver": "^7.6.3", - "teamcity-service-messages": "^0.1.14", - "tsconfig-paths-webpack-plugin": "^4.2.0", - "watskeburt": "^4.2.2" - }, - "bin": { - "depcruise": "bin/dependency-cruise.mjs", - "depcruise-baseline": "bin/depcruise-baseline.mjs", - "depcruise-fmt": "bin/depcruise-fmt.mjs", - "depcruise-wrap-stream-in-html": "bin/wrap-stream-in-html.mjs", - "dependency-cruise": "bin/dependency-cruise.mjs", - "dependency-cruiser": "bin/dependency-cruise.mjs" - }, - "engines": { - "node": "^18.17||>=20" - } - }, - "node_modules/dependency-cruiser/node_modules/ignore": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.3.tgz", - "integrity": "sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/devtools-protocol": { - "version": "0.0.1402036", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1402036.tgz", - "integrity": "sha512-JwAYQgEvm3yD45CHB+RmF5kMbWtXBaOGwuxa87sZogHcLCv8c/IqnThaoQ1y60d7pXWjSKWQphPEc+1rAScVdg==", - "license": "BSD-3-Clause" - }, - "node_modules/dezalgo": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", - "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", - "dev": true, - "license": "ISC", - "dependencies": { - "asap": "^2.0.0", - "wrappy": "1" - } - }, - "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/docker-compose": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/docker-compose/-/docker-compose-1.1.0.tgz", - "integrity": "sha512-VrkQJNafPQ5d6bGULW0P6KqcxSkv3ZU5Wn2wQA19oB71o7+55vQ9ogFe2MMeNbK+jc9rrKVy280DnHO5JLMWOQ==", - "license": "MIT", - "dependencies": { - "yaml": "^2.2.2" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/docker-modem": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.6.tgz", - "integrity": "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ==", - "license": "Apache-2.0", - "dependencies": { - "debug": "^4.1.1", - "readable-stream": "^3.5.0", - "split-ca": "^1.0.1", - "ssh2": "^1.15.0" - }, - "engines": { - "node": ">= 8.0" - } - }, - "node_modules/dockerode": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.4.tgz", - "integrity": "sha512-6GYP/EdzEY50HaOxTVTJ2p+mB5xDHTMJhS+UoGrVyS6VC+iQRh7kZ4FRpUYq6nziby7hPqWhOrFFUFTMUZJJ5w==", - "license": "Apache-2.0", - "dependencies": { - "@balena/dockerignore": "^1.0.2", - "@grpc/grpc-js": "^1.11.1", - "@grpc/proto-loader": "^0.7.13", - "docker-modem": "^5.0.6", - "protobufjs": "^7.3.2", - "tar-fs": "~2.0.1", - "uuid": "^10.0.0" - }, - "engines": { - "node": ">= 8.0" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/ejs": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "jake": "^10.8.5" - }, - "bin": { - "ejs": "bin/cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.96", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.96.tgz", - "integrity": "sha512-8AJUW6dh75Fm/ny8+kZKJzI1pgoE8bKLZlzDU2W1ENd+DXKJrx7I7l9hb8UWR4ojlnb5OlixMt00QWiYJoVw1w==", - "dev": true, - "license": "ISC" - }, - "node_modules/emittery": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", - "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" - } - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/enabled": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", - "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, - "node_modules/encoding/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", - "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.18.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", - "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "license": "MIT", - "optional": true - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/esbuild": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", - "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.23.1", - "@esbuild/android-arm": "0.23.1", - "@esbuild/android-arm64": "0.23.1", - "@esbuild/android-x64": "0.23.1", - "@esbuild/darwin-arm64": "0.23.1", - "@esbuild/darwin-x64": "0.23.1", - "@esbuild/freebsd-arm64": "0.23.1", - "@esbuild/freebsd-x64": "0.23.1", - "@esbuild/linux-arm": "0.23.1", - "@esbuild/linux-arm64": "0.23.1", - "@esbuild/linux-ia32": "0.23.1", - "@esbuild/linux-loong64": "0.23.1", - "@esbuild/linux-mips64el": "0.23.1", - "@esbuild/linux-ppc64": "0.23.1", - "@esbuild/linux-riscv64": "0.23.1", - "@esbuild/linux-s390x": "0.23.1", - "@esbuild/linux-x64": "0.23.1", - "@esbuild/netbsd-x64": "0.23.1", - "@esbuild/openbsd-arm64": "0.23.1", - "@esbuild/openbsd-x64": "0.23.1", - "@esbuild/sunos-x64": "0.23.1", - "@esbuild/win32-arm64": "0.23.1", - "@esbuild/win32-ia32": "0.23.1", - "@esbuild/win32-x64": "0.23.1" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "license": "BSD-2-Clause", - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/eslint": { - "version": "9.20.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.20.0.tgz", - "integrity": "sha512-aL4F8167Hg4IvsW89ejnpTwx+B/UQRzJPGgbIOl+4XqffWsahVVsLEWoZvnrVuwpWmnRd7XeXmQI1zlKcFDteA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.0", - "@eslint/core": "^0.11.0", - "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.20.0", - "@eslint/plugin-kit": "^0.2.5", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.1", - "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.2.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-scope": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", - "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.14.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "license": "(MIT OR WTFPL)", - "engines": { - "node": ">=6" - } - }, - "node_modules/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/expect-utils": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express-rate-limit": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", - "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": "^4.11 || 5 || ^5.0.0-beta.1" - } - }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, - "node_modules/extract-zip/node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", - "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/fastq": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz", - "integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fb-watchman": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bser": "2.1.1" - } - }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, - "node_modules/fecha": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", - "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", - "license": "MIT" - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/file-stream-rotator": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.6.1.tgz", - "integrity": "sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==", - "license": "MIT", - "dependencies": { - "moment": "^2.29.1" - } - }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "license": "MIT" - }, - "node_modules/filelist": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "minimatch": "^5.0.1" - } - }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", - "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", - "dev": true, - "license": "ISC" - }, - "node_modules/fn.name": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", - "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", - "license": "MIT" - }, - "node_modules/form-data": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", - "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/formidable": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.2.tgz", - "integrity": "sha512-Jqc1btCy3QzRbJaICGwKcBfGWuLADRerLzDqi2NwSt/UkXLsHJw2TVResiaoBufHVHy9aSgClOHCeJsSsFLTbg==", - "dev": true, - "license": "MIT", - "dependencies": { - "dezalgo": "^1.0.4", - "hexoid": "^2.0.0", - "once": "^1.4.0" - }, - "funding": { - "url": "https://ko-fi.com/tunnckoCore/commissions" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "license": "MIT" - }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gauge": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", - "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.2", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.1", - "object-assign": "^4.1.1", - "signal-exit": "^3.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", - "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "function-bind": "^1.1.2", - "get-proto": "^1.0.0", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-tsconfig": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", - "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/get-uri": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.4.tgz", - "integrity": "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==", - "license": "MIT", - "dependencies": { - "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.2", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "license": "MIT" - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/global-directory": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", - "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ini": "4.1.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globals": { - "version": "15.14.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz", - "integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "devOptional": true, - "license": "ISC" - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "license": "ISC" - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hexoid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-2.0.0.tgz", - "integrity": "sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true, - "license": "ISC" - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, - "license": "MIT" - }, - "node_modules/http-cache-semantics": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", - "license": "BSD-2-Clause", - "optional": true - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/http-proxy-agent/node_modules/agent-base": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", - "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/https": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz", - "integrity": "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==", - "license": "ISC" - }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "ms": "^2.0.0" - } - }, - "node_modules/i": { - "version": "0.3.7", - "resolved": "https://registry.npmjs.org/i/-/i-0.3.7.tgz", - "integrity": "sha512-FYz4wlXgkQwIPqhzC5TdNMLSE5+GS1IIDJZY/1ZiEPCT2S3COUVZeT5OW4BmW4r5LHLQuOosSwsvnroG9GR59Q==", - "engines": { - "node": ">=0.4" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", - "dev": true, - "license": "ISC" - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-local": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", - "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/infer-owner": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", - "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", - "license": "ISC", - "optional": true - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ini": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", - "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/interpret": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", - "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/ip-address": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", - "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", - "license": "MIT", - "dependencies": { - "jsbn": "1.1.0", - "sprintf-js": "^1.1.3" - }, - "engines": { - "node": ">= 12" - } - }, - "node_modules/ipaddr.js": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", - "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "license": "MIT" - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-installed-globally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-1.0.0.tgz", - "integrity": "sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "global-directory": "^4.0.1", - "is-path-inside": "^4.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-lambda": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", - "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", - "license": "MIT", - "optional": true - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-path-inside": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", - "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "devOptional": true, - "license": "ISC" - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-report/node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-reports": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jake": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", - "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "async": "^3.2.3", - "chalk": "^4.0.2", - "filelist": "^1.0.4", - "minimatch": "^3.1.2" - }, - "bin": { - "jake": "bin/cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jake/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/jake/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", - "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/core": "^29.7.0", - "@jest/types": "^29.6.3", - "import-local": "^3.0.2", - "jest-cli": "^29.7.0" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-changed-files": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", - "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "execa": "^5.0.0", - "jest-util": "^29.7.0", - "p-limit": "^3.1.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-circus": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", - "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "dedent": "^1.0.0", - "is-generator-fn": "^2.0.0", - "jest-each": "^29.7.0", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "p-limit": "^3.1.0", - "pretty-format": "^29.7.0", - "pure-rand": "^6.0.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-cli": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", - "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/core": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "create-jest": "^29.7.0", - "exit": "^0.1.2", - "import-local": "^3.0.2", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "yargs": "^17.3.1" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-config": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", - "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-jest": "^29.7.0", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-circus": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "micromatch": "^4.0.4", - "parse-json": "^5.2.0", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@types/node": "*", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/jest-diff": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-docblock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", - "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "detect-newline": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-each": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", - "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "jest-util": "^29.7.0", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-environment-node": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", - "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-get-type": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-haste-map": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", - "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "micromatch": "^4.0.4", - "walker": "^1.0.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - } - }, - "node_modules/jest-leak-detector": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", - "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-matcher-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", - "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-message-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", - "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.6.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-mock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", - "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-pnp-resolver": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", - "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "peerDependencies": { - "jest-resolve": "*" - }, - "peerDependenciesMeta": { - "jest-resolve": { - "optional": true - } - } - }, - "node_modules/jest-regex-util": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", - "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-resolve": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", - "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "resolve": "^1.20.0", - "resolve.exports": "^2.0.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-resolve-dependencies": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", - "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-regex-util": "^29.6.3", - "jest-snapshot": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-runner": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", - "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/environment": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "graceful-fs": "^4.2.9", - "jest-docblock": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-leak-detector": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-resolve": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-util": "^29.7.0", - "jest-watcher": "^29.7.0", - "jest-worker": "^29.7.0", - "p-limit": "^3.1.0", - "source-map-support": "0.5.13" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-runtime": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", - "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/globals": "^29.7.0", - "@jest/source-map": "^29.6.3", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-snapshot": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", - "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.11.6", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-jsx": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "natural-compare": "^1.4.0", - "pretty-format": "^29.7.0", - "semver": "^7.5.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", - "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-util/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/jest-validate": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", - "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "leven": "^3.1.0", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-validate/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-watcher": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", - "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "jest-util": "^29.7.0", - "string-length": "^4.0.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-worker": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", - "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "jest-util": "^29.7.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsbn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", - "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", - "license": "MIT" - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/kuler": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", - "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", - "license": "MIT" - }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/license-checker": { - "version": "25.0.1", - "resolved": "https://registry.npmjs.org/license-checker/-/license-checker-25.0.1.tgz", - "integrity": "sha512-mET5AIwl7MR2IAKYYoVBBpV0OnkKQ1xGj2IMMeEFIs42QAkEVjRtFZGWmQ28WeU7MP779iAgOaOy93Mn44mn6g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "chalk": "^2.4.1", - "debug": "^3.1.0", - "mkdirp": "^0.5.1", - "nopt": "^4.0.1", - "read-installed": "~4.0.3", - "semver": "^5.5.0", - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0", - "spdx-satisfies": "^4.0.0", - "treeify": "^1.1.0" - }, - "bin": { - "license-checker": "bin/license-checker" - } - }, - "node_modules/license-checker/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/license-checker/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/license-checker/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/license-checker/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true, - "license": "MIT" - }, - "node_modules/license-checker/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/license-checker/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/license-checker/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/license-checker/node_modules/nopt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", - "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", - "dev": true, - "license": "ISC", - "dependencies": { - "abbrev": "1", - "osenv": "^0.1.4" - }, - "bin": { - "nopt": "bin/nopt.js" - } - }, - "node_modules/license-checker/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/license-checker/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "license": "MIT" - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", - "license": "MIT" - }, - "node_modules/lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/logform": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", - "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", - "license": "MIT", - "dependencies": { - "@colors/colors": "1.6.0", - "@types/triple-beam": "^1.3.2", - "fecha": "^4.2.0", - "ms": "^2.1.1", - "safe-stable-stringify": "^2.3.1", - "triple-beam": "^1.3.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/long": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/long/-/long-5.2.4.tgz", - "integrity": "sha512-qtzLbJE8hq7VabR3mISmVGtoXP8KGc2Z/AT8OuqlYD7JTR3oqrgwdjnk07wpj1twXxYmgDXgoKVWUG/fReSzHg==", - "license": "Apache-2.0" - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "license": "MIT", - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true, - "license": "ISC" - }, - "node_modules/make-fetch-happen": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", - "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", - "license": "ISC", - "optional": true, - "dependencies": { - "agentkeepalive": "^4.1.3", - "cacache": "^15.2.0", - "http-cache-semantics": "^4.1.0", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^6.0.0", - "minipass": "^3.1.3", - "minipass-collect": "^1.0.2", - "minipass-fetch": "^1.3.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.2", - "promise-retry": "^2.0.1", - "socks-proxy-agent": "^6.0.0", - "ssri": "^8.0.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/make-fetch-happen/node_modules/http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", - "license": "MIT", - "optional": true, - "dependencies": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/make-fetch-happen/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "license": "ISC", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/make-fetch-happen/node_modules/socks-proxy-agent": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", - "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "agent-base": "^6.0.2", - "debug": "^4.3.3", - "socks": "^2.6.2" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/make-fetch-happen/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC", - "optional": true - }, - "node_modules/makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tmpl": "1.0.5" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/memoize": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/memoize/-/memoize-10.0.0.tgz", - "integrity": "sha512-H6cBLgsi6vMWOcCpvVCdFFnl3kerEXbrYh9q+lY6VXvQSmM6CkmV08VOwT+WE2tzIEqRPFfAq3fm4v/UIW6mSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sindresorhus/memoize?sponsor=1" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-collect": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", - "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-fetch": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", - "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", - "license": "MIT", - "optional": true, - "dependencies": { - "minipass": "^3.1.0", - "minipass-sized": "^1.0.3", - "minizlib": "^2.0.0" - }, - "engines": { - "node": ">=8" - }, - "optionalDependencies": { - "encoding": "^0.1.12" - } - }, - "node_modules/minipass-flush": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", - "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-pipeline": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", - "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-sized": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", - "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, - "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "license": "MIT", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minizlib/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, - "node_modules/mitt": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", - "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", - "license": "MIT" - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "license": "MIT" - }, - "node_modules/moment": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", - "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/nan": { - "version": "2.22.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", - "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", - "license": "MIT", - "optional": true - }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "license": "MIT" - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/netmask": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", - "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/node-abi": { - "version": "3.74.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz", - "integrity": "sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==", - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-addon-api": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", - "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", - "license": "MIT" - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-gyp": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", - "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", - "license": "MIT", - "optional": true, - "dependencies": { - "env-paths": "^2.2.0", - "glob": "^7.1.4", - "graceful-fs": "^4.2.6", - "make-fetch-happen": "^9.1.0", - "nopt": "^5.0.0", - "npmlog": "^6.0.0", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.2", - "which": "^2.0.2" - }, - "bin": { - "node-gyp": "bin/node-gyp.js" - }, - "engines": { - "node": ">= 10.12.0" - } - }, - "node_modules/node-gyp/node_modules/are-we-there-yet": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", - "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "optional": true, - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/node-gyp/node_modules/gauge": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", - "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "optional": true, - "dependencies": { - "aproba": "^1.0.3 || ^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" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/node-gyp/node_modules/npmlog": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", - "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "optional": true, - "dependencies": { - "are-we-there-yet": "^3.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^4.0.3", - "set-blocking": "^2.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "dev": true, - "license": "MIT" - }, - "node_modules/nodemailer": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.0.tgz", - "integrity": "sha512-SQ3wZCExjeSatLE/HBaXS5vqUOQk6GtBdIIKxiFdmm01mOQZX/POJkO3SUX1wDiYcwUOJwT23scFSC9fY2H8IA==", - "license": "MIT-0", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/nodemon": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz", - "integrity": "sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chokidar": "^3.5.2", - "debug": "^4", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.1.2", - "pstree.remy": "^1.1.8", - "semver": "^7.5.3", - "simple-update-notifier": "^2.0.0", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.5" - }, - "bin": { - "nodemon": "bin/nodemon.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nodemon" - } - }, - "node_modules/nodemon/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/nodemon/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/nodemon/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/nodemon/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/nodemon/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/nodemon/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/nodemon/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/nodemon/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "license": "ISC", - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "node_modules/normalize-package-data/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/npm/-/npm-11.1.0.tgz", - "integrity": "sha512-rPMBrZud26lI/LcjQeLw/K5Hf1apXMKgkpNNEzp0YQYmM877+T1ZNKPcB2hnTi7e6fBNz8xLtMMn/w46fVUqGw==", - "bundleDependencies": [ - "@isaacs/string-locale-compare", - "@npmcli/arborist", - "@npmcli/config", - "@npmcli/fs", - "@npmcli/map-workspaces", - "@npmcli/package-json", - "@npmcli/promise-spawn", - "@npmcli/redact", - "@npmcli/run-script", - "@sigstore/tuf", - "abbrev", - "archy", - "cacache", - "chalk", - "ci-info", - "cli-columns", - "fastest-levenshtein", - "fs-minipass", - "glob", - "graceful-fs", - "hosted-git-info", - "ini", - "init-package-json", - "is-cidr", - "json-parse-even-better-errors", - "libnpmaccess", - "libnpmdiff", - "libnpmexec", - "libnpmfund", - "libnpmorg", - "libnpmpack", - "libnpmpublish", - "libnpmsearch", - "libnpmteam", - "libnpmversion", - "make-fetch-happen", - "minimatch", - "minipass", - "minipass-pipeline", - "ms", - "node-gyp", - "nopt", - "normalize-package-data", - "npm-audit-report", - "npm-install-checks", - "npm-package-arg", - "npm-pick-manifest", - "npm-profile", - "npm-registry-fetch", - "npm-user-validate", - "p-map", - "pacote", - "parse-conflict-json", - "proc-log", - "qrcode-terminal", - "read", - "semver", - "spdx-expression-parse", - "ssri", - "supports-color", - "tar", - "text-table", - "tiny-relative-date", - "treeverse", - "validate-npm-package-name", - "which" - ], - "license": "Artistic-2.0", - "workspaces": [ - "docs", - "smoke-tests", - "mock-globals", - "mock-registry", - "workspaces/*" - ], - "dependencies": { - "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/arborist": "^9.0.0", - "@npmcli/config": "^10.0.1", - "@npmcli/fs": "^4.0.0", - "@npmcli/map-workspaces": "^4.0.2", - "@npmcli/package-json": "^6.1.1", - "@npmcli/promise-spawn": "^8.0.2", - "@npmcli/redact": "^3.0.0", - "@npmcli/run-script": "^9.0.1", - "@sigstore/tuf": "^3.0.0", - "abbrev": "^3.0.0", - "archy": "~1.0.0", - "cacache": "^19.0.1", - "chalk": "^5.4.1", - "ci-info": "^4.1.0", - "cli-columns": "^4.0.0", - "fastest-levenshtein": "^1.0.16", - "fs-minipass": "^3.0.3", - "glob": "^10.4.5", - "graceful-fs": "^4.2.11", - "hosted-git-info": "^8.0.2", - "ini": "^5.0.0", - "init-package-json": "^8.0.0", - "is-cidr": "^5.1.0", - "json-parse-even-better-errors": "^4.0.0", - "libnpmaccess": "^10.0.0", - "libnpmdiff": "^8.0.0", - "libnpmexec": "^10.0.0", - "libnpmfund": "^7.0.0", - "libnpmorg": "^8.0.0", - "libnpmpack": "^9.0.0", - "libnpmpublish": "^11.0.0", - "libnpmsearch": "^9.0.0", - "libnpmteam": "^8.0.0", - "libnpmversion": "^8.0.0", - "make-fetch-happen": "^14.0.3", - "minimatch": "^9.0.5", - "minipass": "^7.1.1", - "minipass-pipeline": "^1.2.4", - "ms": "^2.1.2", - "node-gyp": "^11.0.0", - "nopt": "^8.0.0", - "normalize-package-data": "^7.0.0", - "npm-audit-report": "^6.0.0", - "npm-install-checks": "^7.1.1", - "npm-package-arg": "^12.0.1", - "npm-pick-manifest": "^10.0.0", - "npm-profile": "^11.0.1", - "npm-registry-fetch": "^18.0.2", - "npm-user-validate": "^3.0.0", - "p-map": "^7.0.3", - "pacote": "^21.0.0", - "parse-conflict-json": "^4.0.0", - "proc-log": "^5.0.0", - "qrcode-terminal": "^0.12.0", - "read": "^4.0.0", - "semver": "^7.6.3", - "spdx-expression-parse": "^4.0.0", - "ssri": "^12.0.0", - "supports-color": "^9.4.0", - "tar": "^6.2.1", - "text-table": "~0.2.0", - "tiny-relative-date": "^1.3.0", - "treeverse": "^3.0.0", - "validate-npm-package-name": "^6.0.0", - "which": "^5.0.0" - }, - "bin": { - "npm": "bin/npm-cli.js", - "npx": "bin/npx-cli.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm-normalize-package-bin": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", - "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", - "dev": true, - "license": "ISC" - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/@isaacs/cliui": { - "version": "8.0.2", - "inBundle": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/npm/node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.1.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/npm/node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "inBundle": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm/node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/npm/node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/npm/node_modules/@isaacs/string-locale-compare": { - "version": "1.1.0", - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/@npmcli/agent": { - "version": "3.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "agent-base": "^7.1.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.1", - "lru-cache": "^10.0.1", - "socks-proxy-agent": "^8.0.3" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/arborist": { - "version": "9.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/fs": "^4.0.0", - "@npmcli/installed-package-contents": "^3.0.0", - "@npmcli/map-workspaces": "^4.0.1", - "@npmcli/metavuln-calculator": "^9.0.0", - "@npmcli/name-from-folder": "^3.0.0", - "@npmcli/node-gyp": "^4.0.0", - "@npmcli/package-json": "^6.0.1", - "@npmcli/query": "^4.0.0", - "@npmcli/redact": "^3.0.0", - "@npmcli/run-script": "^9.0.1", - "bin-links": "^5.0.0", - "cacache": "^19.0.1", - "common-ancestor-path": "^1.0.1", - "hosted-git-info": "^8.0.0", - "json-stringify-nice": "^1.1.4", - "lru-cache": "^10.2.2", - "minimatch": "^9.0.4", - "nopt": "^8.0.0", - "npm-install-checks": "^7.1.0", - "npm-package-arg": "^12.0.0", - "npm-pick-manifest": "^10.0.0", - "npm-registry-fetch": "^18.0.1", - "pacote": "^21.0.0", - "parse-conflict-json": "^4.0.0", - "proc-log": "^5.0.0", - "proggy": "^3.0.0", - "promise-all-reject-late": "^1.0.0", - "promise-call-limit": "^3.0.1", - "read-package-json-fast": "^4.0.0", - "semver": "^7.3.7", - "ssri": "^12.0.0", - "treeverse": "^3.0.0", - "walk-up-path": "^4.0.0" - }, - "bin": { - "arborist": "bin/index.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@npmcli/config": { - "version": "10.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/map-workspaces": "^4.0.1", - "@npmcli/package-json": "^6.0.1", - "ci-info": "^4.0.0", - "ini": "^5.0.0", - "nopt": "^8.0.0", - "proc-log": "^5.0.0", - "semver": "^7.3.5", - "walk-up-path": "^4.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@npmcli/fs": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/git": { - "version": "6.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/promise-spawn": "^8.0.0", - "ini": "^5.0.0", - "lru-cache": "^10.0.1", - "npm-pick-manifest": "^10.0.0", - "proc-log": "^5.0.0", - "promise-inflight": "^1.0.1", - "promise-retry": "^2.0.1", - "semver": "^7.3.5", - "which": "^5.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/installed-package-contents": { - "version": "3.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-bundled": "^4.0.0", - "npm-normalize-package-bin": "^4.0.0" - }, - "bin": { - "installed-package-contents": "bin/index.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/map-workspaces": { - "version": "4.0.2", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/name-from-folder": "^3.0.0", - "@npmcli/package-json": "^6.0.0", - "glob": "^10.2.2", - "minimatch": "^9.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/metavuln-calculator": { - "version": "9.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "cacache": "^19.0.0", - "json-parse-even-better-errors": "^4.0.0", - "pacote": "^21.0.0", - "proc-log": "^5.0.0", - "semver": "^7.3.5" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@npmcli/name-from-folder": { - "version": "3.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/node-gyp": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/package-json": { - "version": "6.1.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/git": "^6.0.0", - "glob": "^10.2.2", - "hosted-git-info": "^8.0.0", - "json-parse-even-better-errors": "^4.0.0", - "proc-log": "^5.0.0", - "semver": "^7.5.3", - "validate-npm-package-license": "^3.0.4" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/promise-spawn": { - "version": "8.0.2", - "inBundle": true, - "license": "ISC", - "dependencies": { - "which": "^5.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/query": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "postcss-selector-parser": "^6.1.2" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/redact": { - "version": "3.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/run-script": { - "version": "9.0.2", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/node-gyp": "^4.0.0", - "@npmcli/package-json": "^6.0.0", - "@npmcli/promise-spawn": "^8.0.0", - "node-gyp": "^11.0.0", - "proc-log": "^5.0.0", - "which": "^5.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "inBundle": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/npm/node_modules/@sigstore/bundle": { - "version": "3.0.0", - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/protobuf-specs": "^0.3.2" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@sigstore/core": { - "version": "2.0.0", - "inBundle": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@sigstore/protobuf-specs": { - "version": "0.3.3", - "inBundle": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@sigstore/sign": { - "version": "3.0.0", - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/bundle": "^3.0.0", - "@sigstore/core": "^2.0.0", - "@sigstore/protobuf-specs": "^0.3.2", - "make-fetch-happen": "^14.0.1", - "proc-log": "^5.0.0", - "promise-retry": "^2.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@sigstore/tuf": { - "version": "3.0.0", - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/protobuf-specs": "^0.3.2", - "tuf-js": "^3.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@sigstore/verify": { - "version": "2.0.0", - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/bundle": "^3.0.0", - "@sigstore/core": "^2.0.0", - "@sigstore/protobuf-specs": "^0.3.2" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@tufjs/canonical-json": { - "version": "2.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/@tufjs/models": { - "version": "3.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@tufjs/canonical-json": "2.0.0", - "minimatch": "^9.0.5" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/abbrev": { - "version": "3.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/agent-base": { - "version": "7.1.3", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/npm/node_modules/ansi-regex": { - "version": "5.0.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/ansi-styles": { - "version": "6.2.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/npm/node_modules/aproba": { - "version": "2.0.0", - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/archy": { - "version": "1.0.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/balanced-match": { - "version": "1.0.2", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/bin-links": { - "version": "5.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "cmd-shim": "^7.0.0", - "npm-normalize-package-bin": "^4.0.0", - "proc-log": "^5.0.0", - "read-cmd-shim": "^5.0.0", - "write-file-atomic": "^6.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/binary-extensions": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=18.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm/node_modules/brace-expansion": { - "version": "2.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/npm/node_modules/cacache": { - "version": "19.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/fs": "^4.0.0", - "fs-minipass": "^3.0.0", - "glob": "^10.2.2", - "lru-cache": "^10.0.1", - "minipass": "^7.0.3", - "minipass-collect": "^2.0.1", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "p-map": "^7.0.2", - "ssri": "^12.0.0", - "tar": "^7.4.3", - "unique-filename": "^4.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/cacache/node_modules/chownr": { - "version": "3.0.0", - "inBundle": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/npm/node_modules/cacache/node_modules/minizlib": { - "version": "3.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.0.4", - "rimraf": "^5.0.5" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/npm/node_modules/cacache/node_modules/mkdirp": { - "version": "3.0.1", - "inBundle": true, - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/cacache/node_modules/tar": { - "version": "7.4.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.0.1", - "mkdirp": "^3.0.1", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/npm/node_modules/cacache/node_modules/yallist": { - "version": "5.0.0", - "inBundle": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/npm/node_modules/chalk": { - "version": "5.4.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/npm/node_modules/chownr": { - "version": "2.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/npm/node_modules/ci-info": { - "version": "4.1.0", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/cidr-regex": { - "version": "4.1.1", - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "ip-regex": "^5.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/npm/node_modules/cli-columns": { - "version": "4.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/npm/node_modules/cmd-shim": { - "version": "7.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/color-convert": { - "version": "2.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/npm/node_modules/color-name": { - "version": "1.1.4", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/common-ancestor-path": { - "version": "1.0.1", - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/cross-spawn": { - "version": "7.0.6", - "inBundle": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/npm/node_modules/cross-spawn/node_modules/which": { - "version": "2.0.2", - "inBundle": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/npm/node_modules/cssesc": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/npm/node_modules/debug": { - "version": "4.4.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/npm/node_modules/diff": { - "version": "7.0.0", - "inBundle": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/npm/node_modules/eastasianwidth": { - "version": "0.2.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/emoji-regex": { - "version": "8.0.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/encoding": { - "version": "0.1.13", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, - "node_modules/npm/node_modules/env-paths": { - "version": "2.2.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/npm/node_modules/err-code": { - "version": "2.0.3", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/exponential-backoff": { - "version": "3.1.1", - "inBundle": true, - "license": "Apache-2.0" - }, - "node_modules/npm/node_modules/fastest-levenshtein": { - "version": "1.0.16", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 4.9.1" - } - }, - "node_modules/npm/node_modules/foreground-child": { - "version": "3.3.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/fs-minipass": { - "version": "3.0.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/glob": { - "version": "10.4.5", - "inBundle": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/graceful-fs": { - "version": "4.2.11", - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/hosted-git-info": { - "version": "8.0.2", - "inBundle": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^10.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/http-cache-semantics": { - "version": "4.1.1", - "inBundle": true, - "license": "BSD-2-Clause" - }, - "node_modules/npm/node_modules/http-proxy-agent": { - "version": "7.0.2", - "inBundle": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/npm/node_modules/https-proxy-agent": { - "version": "7.0.6", - "inBundle": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/npm/node_modules/iconv-lite": { - "version": "0.6.3", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm/node_modules/ignore-walk": { - "version": "7.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minimatch": "^9.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/imurmurhash": { - "version": "0.1.4", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/npm/node_modules/ini": { - "version": "5.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/init-package-json": { - "version": "8.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/package-json": "^6.1.0", - "npm-package-arg": "^12.0.0", - "promzard": "^2.0.0", - "read": "^4.0.0", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4", - "validate-npm-package-name": "^6.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/ip-address": { - "version": "9.0.5", - "inBundle": true, - "license": "MIT", - "dependencies": { - "jsbn": "1.1.0", - "sprintf-js": "^1.1.3" - }, - "engines": { - "node": ">= 12" - } - }, - "node_modules/npm/node_modules/ip-regex": { - "version": "5.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm/node_modules/is-cidr": { - "version": "5.1.0", - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "cidr-regex": "^4.1.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/npm/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/isexe": { - "version": "2.0.0", - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/jackspeak": { - "version": "3.4.3", - "inBundle": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/npm/node_modules/jsbn": { - "version": "1.1.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/json-parse-even-better-errors": { - "version": "4.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/json-stringify-nice": { - "version": "1.1.4", - "inBundle": true, - "license": "ISC", - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/jsonparse": { - "version": "1.3.1", - "engines": [ - "node >= 0.2.0" - ], - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/just-diff": { - "version": "6.0.2", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/just-diff-apply": { - "version": "5.5.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/libnpmaccess": { - "version": "10.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-package-arg": "^12.0.0", - "npm-registry-fetch": "^18.0.1" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/libnpmdiff": { - "version": "8.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/arborist": "^9.0.0", - "@npmcli/installed-package-contents": "^3.0.0", - "binary-extensions": "^3.0.0", - "diff": "^7.0.0", - "minimatch": "^9.0.4", - "npm-package-arg": "^12.0.0", - "pacote": "^21.0.0", - "tar": "^6.2.1" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/libnpmexec": { - "version": "10.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/arborist": "^9.0.0", - "@npmcli/run-script": "^9.0.1", - "ci-info": "^4.0.0", - "npm-package-arg": "^12.0.0", - "pacote": "^21.0.0", - "proc-log": "^5.0.0", - "read": "^4.0.0", - "read-package-json-fast": "^4.0.0", - "semver": "^7.3.7", - "walk-up-path": "^4.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/libnpmfund": { - "version": "7.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/arborist": "^9.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/libnpmorg": { - "version": "8.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "aproba": "^2.0.0", - "npm-registry-fetch": "^18.0.1" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/libnpmpack": { - "version": "9.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/arborist": "^9.0.0", - "@npmcli/run-script": "^9.0.1", - "npm-package-arg": "^12.0.0", - "pacote": "^21.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/libnpmpublish": { - "version": "11.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "ci-info": "^4.0.0", - "normalize-package-data": "^7.0.0", - "npm-package-arg": "^12.0.0", - "npm-registry-fetch": "^18.0.1", - "proc-log": "^5.0.0", - "semver": "^7.3.7", - "sigstore": "^3.0.0", - "ssri": "^12.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/libnpmsearch": { - "version": "9.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-registry-fetch": "^18.0.1" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/libnpmteam": { - "version": "8.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "aproba": "^2.0.0", - "npm-registry-fetch": "^18.0.1" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/libnpmversion": { - "version": "8.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/git": "^6.0.1", - "@npmcli/run-script": "^9.0.1", - "json-parse-even-better-errors": "^4.0.0", - "proc-log": "^5.0.0", - "semver": "^7.3.7" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/lru-cache": { - "version": "10.4.3", - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/make-fetch-happen": { - "version": "14.0.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/agent": "^3.0.0", - "cacache": "^19.0.1", - "http-cache-semantics": "^4.1.1", - "minipass": "^7.0.2", - "minipass-fetch": "^4.0.0", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^1.0.0", - "proc-log": "^5.0.0", - "promise-retry": "^2.0.1", - "ssri": "^12.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/make-fetch-happen/node_modules/negotiator": { - "version": "1.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/npm/node_modules/minimatch": { - "version": "9.0.5", - "inBundle": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/minipass": { - "version": "7.1.2", - "inBundle": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/npm/node_modules/minipass-collect": { - "version": "2.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/npm/node_modules/minipass-fetch": { - "version": "4.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.0.3", - "minipass-sized": "^1.0.3", - "minizlib": "^3.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - }, - "optionalDependencies": { - "encoding": "^0.1.13" - } - }, - "node_modules/npm/node_modules/minipass-fetch/node_modules/minizlib": { - "version": "3.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.0.4", - "rimraf": "^5.0.5" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/npm/node_modules/minipass-flush": { - "version": "1.0.5", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/npm/node_modules/minipass-flush/node_modules/minipass": { - "version": "3.3.6", - "inBundle": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/minipass-pipeline": { - "version": "1.2.4", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/minipass-pipeline/node_modules/minipass": { - "version": "3.3.6", - "inBundle": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/minipass-sized": { - "version": "1.0.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/minipass-sized/node_modules/minipass": { - "version": "3.3.6", - "inBundle": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/minizlib": { - "version": "2.1.2", - "inBundle": true, - "license": "MIT", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/npm/node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "inBundle": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/mkdirp": { - "version": "1.0.4", - "inBundle": true, - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/npm/node_modules/ms": { - "version": "2.1.3", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/mute-stream": { - "version": "2.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/node-gyp": { - "version": "11.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.0", - "exponential-backoff": "^3.1.1", - "glob": "^10.3.10", - "graceful-fs": "^4.2.6", - "make-fetch-happen": "^14.0.3", - "nopt": "^8.0.0", - "proc-log": "^5.0.0", - "semver": "^7.3.5", - "tar": "^7.4.3", - "which": "^5.0.0" - }, - "bin": { - "node-gyp": "bin/node-gyp.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/chownr": { - "version": "3.0.0", - "inBundle": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/minizlib": { - "version": "3.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.0.4", - "rimraf": "^5.0.5" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/mkdirp": { - "version": "3.0.1", - "inBundle": true, - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/tar": { - "version": "7.4.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.0.1", - "mkdirp": "^3.0.1", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/yallist": { - "version": "5.0.0", - "inBundle": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/npm/node_modules/nopt": { - "version": "8.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "abbrev": "^2.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/nopt/node_modules/abbrev": { - "version": "2.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/normalize-package-data": { - "version": "7.0.0", - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "hosted-git-info": "^8.0.0", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/npm-audit-report": { - "version": "6.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/npm-bundled": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-normalize-package-bin": "^4.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/npm-install-checks": { - "version": "7.1.1", - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "semver": "^7.1.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/npm-normalize-package-bin": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/npm-package-arg": { - "version": "12.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "hosted-git-info": "^8.0.0", - "proc-log": "^5.0.0", - "semver": "^7.3.5", - "validate-npm-package-name": "^6.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/npm-packlist": { - "version": "10.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "ignore-walk": "^7.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/npm-pick-manifest": { - "version": "10.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-install-checks": "^7.1.0", - "npm-normalize-package-bin": "^4.0.0", - "npm-package-arg": "^12.0.0", - "semver": "^7.3.5" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/npm-profile": { - "version": "11.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-registry-fetch": "^18.0.0", - "proc-log": "^5.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/npm-registry-fetch": { - "version": "18.0.2", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/redact": "^3.0.0", - "jsonparse": "^1.3.1", - "make-fetch-happen": "^14.0.0", - "minipass": "^7.0.2", - "minipass-fetch": "^4.0.0", - "minizlib": "^3.0.1", - "npm-package-arg": "^12.0.0", - "proc-log": "^5.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/npm-registry-fetch/node_modules/minizlib": { - "version": "3.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.0.4", - "rimraf": "^5.0.5" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/npm/node_modules/npm-user-validate": { - "version": "3.0.0", - "inBundle": true, - "license": "BSD-2-Clause", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/p-map": { - "version": "7.0.3", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm/node_modules/package-json-from-dist": { - "version": "1.0.1", - "inBundle": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/npm/node_modules/pacote": { - "version": "21.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/git": "^6.0.0", - "@npmcli/installed-package-contents": "^3.0.0", - "@npmcli/package-json": "^6.0.0", - "@npmcli/promise-spawn": "^8.0.0", - "@npmcli/run-script": "^9.0.0", - "cacache": "^19.0.0", - "fs-minipass": "^3.0.0", - "minipass": "^7.0.2", - "npm-package-arg": "^12.0.0", - "npm-packlist": "^10.0.0", - "npm-pick-manifest": "^10.0.0", - "npm-registry-fetch": "^18.0.0", - "proc-log": "^5.0.0", - "promise-retry": "^2.0.1", - "sigstore": "^3.0.0", - "ssri": "^12.0.0", - "tar": "^6.1.11" - }, - "bin": { - "pacote": "bin/index.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/parse-conflict-json": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "json-parse-even-better-errors": "^4.0.0", - "just-diff": "^6.0.0", - "just-diff-apply": "^5.2.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/path-key": { - "version": "3.1.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/path-scurry": { - "version": "1.11.1", - "inBundle": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/postcss-selector-parser": { - "version": "6.1.2", - "inBundle": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/npm/node_modules/proc-log": { - "version": "5.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/proggy": { - "version": "3.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/promise-all-reject-late": { - "version": "1.0.1", - "inBundle": true, - "license": "ISC", - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/promise-call-limit": { - "version": "3.0.2", - "inBundle": true, - "license": "ISC", - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/promise-inflight": { - "version": "1.0.1", - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/promise-retry": { - "version": "2.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/npm/node_modules/promzard": { - "version": "2.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "read": "^4.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/qrcode-terminal": { - "version": "0.12.0", - "inBundle": true, - "bin": { - "qrcode-terminal": "bin/qrcode-terminal.js" - } - }, - "node_modules/npm/node_modules/read": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "mute-stream": "^2.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/read-cmd-shim": { - "version": "5.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/read-package-json-fast": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "json-parse-even-better-errors": "^4.0.0", - "npm-normalize-package-bin": "^4.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/retry": { - "version": "0.12.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/npm/node_modules/rimraf": { - "version": "5.0.10", - "inBundle": true, - "license": "ISC", - "dependencies": { - "glob": "^10.3.7" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/safer-buffer": { - "version": "2.1.2", - "inBundle": true, - "license": "MIT", - "optional": true - }, - "node_modules/npm/node_modules/semver": { - "version": "7.6.3", - "inBundle": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/npm/node_modules/shebang-command": { - "version": "2.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/shebang-regex": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/signal-exit": { - "version": "4.1.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/sigstore": { - "version": "3.0.0", - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/bundle": "^3.0.0", - "@sigstore/core": "^2.0.0", - "@sigstore/protobuf-specs": "^0.3.2", - "@sigstore/sign": "^3.0.0", - "@sigstore/tuf": "^3.0.0", - "@sigstore/verify": "^2.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/smart-buffer": { - "version": "4.2.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/npm/node_modules/socks": { - "version": "2.8.3", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ip-address": "^9.0.5", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/npm/node_modules/socks-proxy-agent": { - "version": "8.0.5", - "inBundle": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/npm/node_modules/spdx-correct": { - "version": "3.2.0", - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/npm/node_modules/spdx-correct/node_modules/spdx-expression-parse": { - "version": "3.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/npm/node_modules/spdx-exceptions": { - "version": "2.5.0", - "inBundle": true, - "license": "CC-BY-3.0" - }, - "node_modules/npm/node_modules/spdx-expression-parse": { - "version": "4.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/npm/node_modules/spdx-license-ids": { - "version": "3.0.21", - "inBundle": true, - "license": "CC0-1.0" - }, - "node_modules/npm/node_modules/sprintf-js": { - "version": "1.1.3", - "inBundle": true, - "license": "BSD-3-Clause" - }, - "node_modules/npm/node_modules/ssri": { - "version": "12.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/string-width": { - "version": "4.2.3", - "inBundle": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "inBundle": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/strip-ansi": { - "version": "6.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/supports-color": { - "version": "9.4.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/npm/node_modules/tar": { - "version": "6.2.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/npm/node_modules/tar/node_modules/fs-minipass": { - "version": "2.1.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/npm/node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "inBundle": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/tar/node_modules/minipass": { - "version": "5.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/text-table": { - "version": "0.2.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/tiny-relative-date": { - "version": "1.3.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/treeverse": { - "version": "3.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/tuf-js": { - "version": "3.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@tufjs/models": "3.0.1", - "debug": "^4.3.6", - "make-fetch-happen": "^14.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/unique-filename": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "unique-slug": "^5.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/unique-slug": { - "version": "5.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/util-deprecate": { - "version": "1.0.2", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/validate-npm-package-license": { - "version": "3.0.4", - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/npm/node_modules/validate-npm-package-license/node_modules/spdx-expression-parse": { - "version": "3.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/npm/node_modules/validate-npm-package-name": { - "version": "6.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/walk-up-path": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/npm/node_modules/which": { - "version": "5.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/which/node_modules/isexe": { - "version": "3.1.1", - "inBundle": true, - "license": "ISC", - "engines": { - "node": ">=16" - } - }, - "node_modules/npm/node_modules/wrap-ansi": { - "version": "8.1.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/npm/node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/npm/node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/npm/node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.1.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/npm/node_modules/wrap-ansi/node_modules/emoji-regex": { - "version": "9.2.2", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/wrap-ansi/node_modules/string-width": { - "version": "5.1.2", - "inBundle": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm/node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.1.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/npm/node_modules/write-file-atomic": { - "version": "6.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/yallist": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC" - }, - "node_modules/npmlog": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", - "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "dependencies": { - "are-we-there-yet": "^2.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^3.0.0", - "set-blocking": "^2.0.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/one-time": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", - "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", - "license": "MIT", - "dependencies": { - "fn.name": "1.x.x" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "deprecated": "This package is no longer supported.", - "dev": true, - "license": "ISC", - "dependencies": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/pac-proxy-agent": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.1.0.tgz", - "integrity": "sha512-Z5FnLVVZSnX7WjBg0mhDtydeRZ1xMcATZThjySQUHqr+0ksP8kqaw23fNKkaaN/Z8gwLUs/W7xdl0I75eP2Xyw==", - "license": "MIT", - "dependencies": { - "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "get-uri": "^6.0.1", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.6", - "pac-resolver": "^7.0.1", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-proxy-agent/node_modules/agent-base": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", - "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-proxy-agent/node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-resolver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", - "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", - "license": "MIT", - "dependencies": { - "degenerator": "^5.0.0", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "license": "MIT" - }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pirates": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", - "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", - "license": "ISC", - "optional": true - }, - "node_modules/promise-retry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "license": "MIT", - "optional": true, - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/protobufjs": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", - "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/node": ">=13.7.0", - "long": "^5.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/proxy-addr/node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/proxy-agent": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", - "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.6", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.1.0", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-agent/node_modules/agent-base": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", - "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-agent/node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-agent/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, - "node_modules/pstree.remy": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", - "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", - "dev": true, - "license": "MIT" - }, - "node_modules/pump": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", - "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/puppeteer": { - "version": "24.2.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.2.0.tgz", - "integrity": "sha512-z8vv7zPEgrilIbOo3WNvM+2mXMnyM9f4z6zdrB88Fzeuo43Oupmjrzk3EpuvuCtyK0A7Lsllfx7Z+4BvEEGJcQ==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@puppeteer/browsers": "2.7.1", - "chromium-bidi": "1.2.0", - "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1402036", - "puppeteer-core": "24.2.0", - "typed-query-selector": "^2.12.0" - }, - "bin": { - "puppeteer": "lib/cjs/puppeteer/node/cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/puppeteer-core": { - "version": "24.2.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.2.0.tgz", - "integrity": "sha512-e4A4/xqWdd4kcE6QVHYhJ+Qlx/+XpgjP4d8OwBx0DJoY/nkIRhSgYmKQnv7+XSs1ofBstalt+XPGrkaz4FoXOQ==", - "license": "Apache-2.0", - "dependencies": { - "@puppeteer/browsers": "2.7.1", - "chromium-bidi": "1.2.0", - "debug": "^4.4.0", - "devtools-protocol": "0.0.1402036", - "typed-query-selector": "^2.12.0", - "ws": "^8.18.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/pure-rand": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", - "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" - }, - "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/rc/node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC" - }, - "node_modules/rc/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/read-installed": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/read-installed/-/read-installed-4.0.3.tgz", - "integrity": "sha512-O03wg/IYuV/VtnK2h/KXEt9VIbMUFbk3ERG0Iu4FhLZw0EP0T9znqrYDGn6ncbEsXUFaUjiVAWXHzxwt3lhRPQ==", - "deprecated": "This package is no longer supported.", - "dev": true, - "license": "ISC", - "dependencies": { - "debuglog": "^1.0.1", - "read-package-json": "^2.0.0", - "readdir-scoped-modules": "^1.0.0", - "semver": "2 || 3 || 4 || 5", - "slide": "~1.1.3", - "util-extend": "^1.0.1" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.2" - } - }, - "node_modules/read-installed/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/read-package-json": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-2.1.2.tgz", - "integrity": "sha512-D1KmuLQr6ZSJS0tW8hf3WGpRlwszJOXZ3E8Yd/DNRaM5d+1wVRZdHlpGBLAuovjr28LbWvjpWkBHMxpRGGjzNA==", - "deprecated": "This package is no longer supported. Please use @npmcli/package-json instead.", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.1", - "json-parse-even-better-errors": "^2.3.0", - "normalize-package-data": "^2.0.0", - "npm-normalize-package-bin": "^1.0.0" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readdir-scoped-modules": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz", - "integrity": "sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw==", - "deprecated": "This functionality has been moved to @npmcli/fs", - "dev": true, - "license": "ISC", - "dependencies": { - "debuglog": "^1.0.1", - "dezalgo": "^1.0.0", - "graceful-fs": "^4.1.2", - "once": "^1.3.0" - } - }, - "node_modules/readdirp": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.1.tgz", - "integrity": "sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==", - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/rechoir": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", - "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve": "^1.20.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/regexp-tree": { - "version": "0.1.27", - "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", - "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", - "dev": true, - "license": "MIT", - "bin": { - "regexp-tree": "bin/regexp-tree" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-cwd/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "node_modules/resolve.exports": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", - "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safe-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz", - "integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==", - "dev": true, - "license": "MIT", - "dependencies": { - "regexp-tree": "~0.1.1" - } - }, - "node_modules/safe-stable-stringify": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", - "license": "MIT", - "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" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "license": "ISC" - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC" - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, - "node_modules/simple-swizzle/node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", - "license": "MIT" - }, - "node_modules/simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true, - "license": "MIT" - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/slide": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", - "integrity": "sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==", - "dev": true, - "license": "ISC", - "engines": { - "node": "*" - } - }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "license": "MIT", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", - "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", - "license": "MIT", - "dependencies": { - "ip-address": "^9.0.5", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/socks-proxy-agent/node_modules/agent-base": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", - "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "devOptional": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/spdx-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/spdx-compare/-/spdx-compare-1.0.0.tgz", - "integrity": "sha512-C1mDZOX0hnu0ep9dfmuoi03+eOdDoz2yvK79RxbcrVEG1NO1Ph35yW102DHWKN4pk80nwCgeMmSY5L25VE4D9A==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-find-index": "^1.0.2", - "spdx-expression-parse": "^3.0.0", - "spdx-ranges": "^2.0.0" - } - }, - "node_modules/spdx-correct": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", - "dev": true, - "license": "CC-BY-3.0" - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.21", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", - "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/spdx-ranges": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/spdx-ranges/-/spdx-ranges-2.1.1.tgz", - "integrity": "sha512-mcdpQFV7UDAgLpXEE/jOMqvK4LBoO0uTQg0uvXUewmEFhpiZx5yJSZITHB8w1ZahKdhfZqP5GPEOKLyEq5p8XA==", - "dev": true, - "license": "(MIT AND CC-BY-3.0)" - }, - "node_modules/spdx-satisfies": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/spdx-satisfies/-/spdx-satisfies-4.0.1.tgz", - "integrity": "sha512-WVzZ/cXAzoNmjCWiEluEA3BjHp5tiUmmhn9MK+X0tBbR9sOqtC6UQwmgCNrAIZvNlMuBUYAaHYfb2oqlF9SwKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "spdx-compare": "^1.0.0", - "spdx-expression-parse": "^3.0.0", - "spdx-ranges": "^2.0.0" - } - }, - "node_modules/split-ca": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", - "integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==", - "license": "ISC" - }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "license": "BSD-3-Clause" - }, - "node_modules/sqlite3": { - "version": "5.1.7", - "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", - "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "dependencies": { - "bindings": "^1.5.0", - "node-addon-api": "^7.0.0", - "prebuild-install": "^7.1.1", - "tar": "^6.1.11" - }, - "optionalDependencies": { - "node-gyp": "8.x" - }, - "peerDependencies": { - "node-gyp": "8.x" - }, - "peerDependenciesMeta": { - "node-gyp": { - "optional": true - } - } - }, - "node_modules/sqlite3/node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "license": "MIT" - }, - "node_modules/ssh2": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.16.0.tgz", - "integrity": "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg==", - "hasInstallScript": true, - "dependencies": { - "asn1": "^0.2.6", - "bcrypt-pbkdf": "^1.0.2" - }, - "engines": { - "node": ">=10.16.0" - }, - "optionalDependencies": { - "cpu-features": "~0.0.10", - "nan": "^2.20.0" - } - }, - "node_modules/ssri": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", - "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^3.1.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/stack-trace": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/stack-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/stack-utils/node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/streamx": { - "version": "2.22.0", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.0.tgz", - "integrity": "sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==", - "license": "MIT", - "dependencies": { - "fast-fifo": "^1.3.2", - "text-decoder": "^1.1.0" - }, - "optionalDependencies": { - "bare-events": "^2.2.0" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/superagent": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-9.0.2.tgz", - "integrity": "sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "component-emitter": "^1.3.0", - "cookiejar": "^2.1.4", - "debug": "^4.3.4", - "fast-safe-stringify": "^2.1.1", - "form-data": "^4.0.0", - "formidable": "^3.5.1", - "methods": "^1.1.2", - "mime": "2.6.0", - "qs": "^6.11.0" - }, - "engines": { - "node": ">=14.18.0" - } - }, - "node_modules/superagent/node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/supertest": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.0.0.tgz", - "integrity": "sha512-qlsr7fIC0lSddmA3tzojvzubYxvlGtzumcdHgPwbFWMISQwL22MhM2Y3LNt+6w9Yyx7559VW5ab70dgphm8qQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "methods": "^1.1.2", - "superagent": "^9.0.1" - }, - "engines": { - "node": ">=14.18.0" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/swagger-ui-dist": { - "version": "5.18.3", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.18.3.tgz", - "integrity": "sha512-G33HFW0iFNStfY2x6QXO2JYVMrFruc8AZRX0U/L71aA7WeWfX2E5Nm8E/tsipSZJeIZZbSjUDeynLK/wcuNWIw==", - "license": "Apache-2.0", - "dependencies": { - "@scarf/scarf": "=1.4.0" - } - }, - "node_modules/swagger-ui-express": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", - "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", - "license": "MIT", - "dependencies": { - "swagger-ui-dist": ">=5.0.0" - }, - "engines": { - "node": ">= v0.10.32" - }, - "peerDependencies": { - "express": ">=4.0.0 || >=5.0.0-beta" - } - }, - "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "license": "ISC", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/tar-fs": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz", - "integrity": "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==", - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.0.0" - } - }, - "node_modules/tar-fs/node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC" - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tar/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "license": "ISC", - "engines": { - "node": ">=8" - } - }, - "node_modules/tar/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/tar/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, - "node_modules/teamcity-service-messages": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/teamcity-service-messages/-/teamcity-service-messages-0.1.14.tgz", - "integrity": "sha512-29aQwaHqm8RMX74u2o/h1KbMLP89FjNiMxD9wbF2BbWOnbM+q+d1sCEC+MqCc4QW3NJykn77OMpTFw/xTHIc0w==", - "dev": true, - "license": "MIT" - }, - "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/text-decoder": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", - "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", - "license": "Apache-2.0", - "dependencies": { - "b4a": "^1.6.4" - } - }, - "node_modules/text-hex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", - "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", - "license": "MIT" - }, - "node_modules/tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/touch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", - "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", - "dev": true, - "license": "ISC", - "bin": { - "nodetouch": "bin/nodetouch.js" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/treeify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/treeify/-/treeify-1.1.0.tgz", - "integrity": "sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/triple-beam": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", - "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", - "license": "MIT", - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/ts-api-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", - "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/ts-jest": { - "version": "29.2.5", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", - "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "bs-logger": "^0.2.6", - "ejs": "^3.1.10", - "fast-json-stable-stringify": "^2.1.0", - "jest-util": "^29.0.0", - "json5": "^2.2.3", - "lodash.memoize": "^4.1.2", - "make-error": "^1.3.6", - "semver": "^7.6.3", - "yargs-parser": "^21.1.1" - }, - "bin": { - "ts-jest": "cli.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" - }, - "peerDependencies": { - "@babel/core": ">=7.0.0-beta.0 <8", - "@jest/transform": "^29.0.0", - "@jest/types": "^29.0.0", - "babel-jest": "^29.0.0", - "jest": "^29.0.0", - "typescript": ">=4.3 <6" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "@jest/transform": { - "optional": true - }, - "@jest/types": { - "optional": true - }, - "babel-jest": { - "optional": true - }, - "esbuild": { - "optional": true - } - } - }, - "node_modules/ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, - "node_modules/tsconfig-paths": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", - "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "json5": "^2.2.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tsconfig-paths-webpack-plugin": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz", - "integrity": "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "enhanced-resolve": "^5.7.0", - "tapable": "^2.2.1", - "tsconfig-paths": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/tsconfig-paths/node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/tsx": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz", - "integrity": "sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "~0.23.0", - "get-tsconfig": "^4.7.5" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", - "license": "Unlicense" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typed-query-selector": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", - "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", - "license": "MIT" - }, - "node_modules/typescript": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", - "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", - "devOptional": true, - "license": "Apache-2.0", - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-eslint": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.23.0.tgz", - "integrity": "sha512-/LBRo3HrXr5LxmrdYSOCvoAMm7p2jNizNfbIpCgvG4HMsnoprRUOce/+8VJ9BDYWW68rqIENE/haVLWPeFZBVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.23.0", - "@typescript-eslint/parser": "8.23.0", - "@typescript-eslint/utils": "8.23.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" - } - }, - "node_modules/uglify-js": { - "version": "3.19.3", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", - "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/undefsafe": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", - "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", - "dev": true, - "license": "MIT" - }, - "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "license": "MIT" - }, - "node_modules/unique-filename": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", - "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", - "license": "ISC", - "optional": true, - "dependencies": { - "unique-slug": "^2.0.0" - } - }, - "node_modules/unique-slug": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", - "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", - "license": "ISC", - "optional": true, - "dependencies": { - "imurmurhash": "^0.1.4" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", - "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/util-extend": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/util-extend/-/util-extend-1.0.3.tgz", - "integrity": "sha512-mLs5zAK+ctllYBj+iAQvlDCwoxU/WDOUaJkcFudeiAX6OajC6BKXJUa9a+tbtkC11dz2Ufb7h0lyvIOVn4LADA==", - "dev": true, - "license": "MIT" - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true, - "license": "MIT" - }, - "node_modules/v8-to-istanbul": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", - "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", - "dev": true, - "license": "ISC", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^2.0.0" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "makeerror": "1.0.12" - } - }, - "node_modules/watskeburt": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/watskeburt/-/watskeburt-4.2.2.tgz", - "integrity": "sha512-AOCg1UYxWpiHW1tUwqpJau8vzarZYTtzl2uu99UptBmbzx6kOzCGMfRLF6KIRX4PYekmryn89MzxlRNkL66YyA==", - "dev": true, - "license": "MIT", - "bin": { - "watskeburt": "dist/run-cli.js" - }, - "engines": { - "node": "^18||>=20" - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "devOptional": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "license": "ISC", - "dependencies": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, - "node_modules/winston": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", - "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", - "license": "MIT", - "dependencies": { - "@colors/colors": "^1.6.0", - "@dabh/diagnostics": "^2.0.2", - "async": "^3.2.3", - "is-stream": "^2.0.0", - "logform": "^2.7.0", - "one-time": "^1.0.0", - "readable-stream": "^3.4.0", - "safe-stable-stringify": "^2.3.1", - "stack-trace": "0.0.x", - "triple-beam": "^1.3.0", - "winston-transport": "^4.9.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/winston-daily-rotate-file": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-5.0.0.tgz", - "integrity": "sha512-JDjiXXkM5qvwY06733vf09I2wnMXpZEhxEVOSPenZMii+g7pcDcTBt2MRugnoi8BwVSuCT2jfRXBUy+n1Zz/Yw==", - "license": "MIT", - "dependencies": { - "file-stream-rotator": "^0.6.1", - "object-hash": "^3.0.0", - "triple-beam": "^1.4.1", - "winston-transport": "^4.7.0" - }, - "engines": { - "node": ">=8" - }, - "peerDependencies": { - "winston": "^3" - } - }, - "node_modules/winston-transport": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", - "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", - "license": "MIT", - "dependencies": { - "logform": "^2.7.0", - "readable-stream": "^3.6.2", - "triple-beam": "^1.3.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/write-file-atomic": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", - "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "license": "MIT", - "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 - } - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/yaml": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", - "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/yamljs": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz", - "integrity": "sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==", - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "glob": "^7.0.5" - }, - "bin": { - "json2yaml": "bin/json2yaml", - "yaml2json": "bin/yaml2json" - } - }, - "node_modules/yamljs/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/yamljs/node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "license": "BSD-3-Clause" - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zod": { - "version": "3.24.1", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", - "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - } - } -} diff --git a/package.json b/package.json index c48ee73..0e1deb8 100644 --- a/package.json +++ b/package.json @@ -1,123 +1,19 @@ { "name": "dockstatapi", - "repository": "git@github.com:Its4Nik/dockstatapi.git", - "version": "2.0.1", - "description": "API for docker hosts using dockerode", - "main": "src/server.ts", + "version": "2.1.0", "scripts": { - "test": "NODE_ENV=testing jest -w 1 --forceExit", - "test:silent": "NODE_ENV=testing jest -w 1 --forceExit --silent", - "local-env-file": "bash ./src/misc/createEnvDev.sh", - "start": "npm run local-env-file && NODE_ENV=production tsx src/server.ts", - "start:build": "npm run local-env-file -d && npm run build && NODE_ENV=production node dist/src/src/server.js", - "dev": "npm run local-env-file && NODE_ENV=development nodemon", - "dev:socket": "docker compose -f docker/docker-compose.dev.yaml up -d && npm run local-env-file && NODE_ENV=development nodemon ; docker compose -f docker/docker-compose.dev.yaml down", - "dev:trace": "npm run local-env-file && NODE_ENV=development nodemon --trace-uncaught --trace-warnings", - "dep": "bash ./src/misc/dependencyGraphs/createDependencyGraph.sh", - "dep:remove": "bash ./src/misc/removeUnusedDeps.sh && npm run dep", - "build": "tsc", - "build:mini": "tsc && bash ./src/misc/minifyDist.sh --build-only", - "build:docker": "docker build . -t \"dockstatapi:local\" -f ./docker/Dockerfile-dev", - "build:docker:prod": "docker build . -t \"dockstatapi:local\" -f ./docker/Dockerfile-base", - "mini": "bash ./src/misc/minifyDist.sh", - "docker": "docker compose -f docker/docker-compose.yaml up -d && bash ./src/misc/.tmux.sh; docker compose -f docker/docker-compose.yaml down", - "docker:build": "npm run build:docker && npm run docker", - "docker:build:prod": "npm run build:docker:prod && npm run docker", - "prettier": "prettier -c ./__tests__/*.spec.ts --parser typescript --write && prettier -c ./src/**/*.ts --parser typescript --write && prettier -c ./.github/workflows/*.yaml --parser yaml --write && prettier -c ./**/*.md --parser markdown --write && prettier -c ./**/*.json --parser json --write", - "lint": "eslint", - "lint:fix": "eslint --fix", - "license": "bash ./src/misc/credits.sh", - "finish": "npm run local-env-file && npm run license && npm run prettier && npm run lint" + "test": "echo \"Error: no test specified\" && exit 1", + "dev": "bun run --watch src/index.ts" }, - "keywords": [], - "author": "Its4Nik", - "license": "BSD 3-Clause License", "dependencies": { - "bcrypt": "^5.1.1", - "chokidar": "^4.0.1", - "cors": "^2.8.5", - "cytoscape": "^3.30.4", - "docker-compose": "^1.1.0", - "dockerode": "^4.0.2", - "express": "^4.21.1", - "express-rate-limit": "^7.4.1", - "https": "^1.0.0", - "i": "^0.3.7", - "ipaddr.js": "^2.2.0", - "nodemailer": "^6.9.16", - "npm": "^11.0.0", - "puppeteer": "^24.0.0", - "sqlite3": "^5.1.7", - "swagger-ui-express": "^5.0.1", - "winston": "^3.15.0", - "winston-daily-rotate-file": "^5.0.0", - "yamljs": "^0.3.0" + "@elysiajs/swagger": "^1.2.2", + "chalk": "^5.4.1", + "elysia": "latest", + "winston": "^3.17.0", + "winston-transport": "^4.9.0" }, "devDependencies": { - "@eslint/js": "^9.17.0", - "@types/bcrypt": "^5.0.2", - "@types/cors": "^2.8.17", - "@types/cytoscape": "^3.21.8", - "@types/dockerode": "^3.3.31", - "@types/express": "^5.0.0", - "@types/express-handlebars": "^5.3.1", - "@types/jest": "^29.5.14", - "@types/node": "^22.9.0", - "@types/node-fetch": "^2.6.12", - "@types/nodemailer": "^6.4.17", - "@types/supertest": "^6.0.2", - "@types/supports-color": "^8.1.3", - "@types/swagger-jsdoc": "^6.0.4", - "@types/swagger-ui-express": "^4.1.7", - "@types/ws": "^8.5.14", - "@types/yamljs": "^0.2.34", - "@typescript-eslint/eslint-plugin": "^8.18.2", - "@typescript-eslint/parser": "^8.18.2", - "dependency-cruiser": "^16.5.0", - "eslint": "^9.17.0", - "globals": "^15.14.0", - "jest": "^29.7.0", - "license-checker": "^25.0.1", - "nodemon": "^3.1.7", - "prettier": "^3.4.2", - "supertest": "^7.0.0", - "ts-jest": "^29.2.5", - "ts-node": "^10.9.2", - "tsx": "^4.19.2", - "typescript-eslint": "^8.18.2", - "uglify-js": "^3.19.3" + "bun-types": "latest" }, - "engines": { - "npm": ">=10.8.2" - }, - "jest": { - "preset": "ts-jest", - "testMatch": [ - "**/__tests__/**/*.(test|spec).ts" - ], - "testEnvironment": "node", - "transform": { - "^.+\\.(ts|tsx)$": "ts-jest" - }, - "moduleFileExtensions": [ - "ts", - "tsx", - "js", - "jsx", - "json", - "node" - ], - "coveragePathIgnorePatterns": [ - "/node_modules/" - ], - "moduleNameMapper": { - "^@/(.*)$": "src/$1" - }, - "transformIgnorePatterns": [ - "/node_modules/" - ], - "testPathIgnorePatterns": [ - "util" - ] - } + "module": "src/index.js" } diff --git a/src/config/db.ts b/src/config/db.ts deleted file mode 100644 index 5ed4d6a..0000000 --- a/src/config/db.ts +++ /dev/null @@ -1,23 +0,0 @@ -import sqlite3 from "sqlite3"; -import logger from "../utils/logger"; - -const dbPath: string = "./src/data/database.db"; - -const db: sqlite3.Database = new sqlite3.Database(dbPath, (error: unknown) => { - if (error as Error) { - logger.error("Error opening database:", (error as Error).message); - } else { - db.run( - `CREATE TABLE IF NOT EXISTS data ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - info TEXT NOT NULL, - timestamp DATETIME DEFAULT CURRENT_TIMESTAMP - )`, - () => { - logger.info("Database created / checked successfully, table is ready."); - }, - ); - } -}); - -export default db; diff --git a/src/config/hostsystem.ts b/src/config/hostsystem.ts deleted file mode 100644 index 87928a8..0000000 --- a/src/config/hostsystem.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { - RUNNING_IN_DOCKER, - VERSION, - HA_MASTER, - HA_UNSAFE, - TRUSTED_PROXIES, - LOG_LEVEL, -} from "./variables"; -import fs from "fs"; -import logger from "../utils/logger"; -import os from "os"; -import { atomicWrite } from "../utils/atomicWrite"; - -const userConf = "./src/data/user.conf"; -const inDocker: boolean = RUNNING_IN_DOCKER == "true"; -const version: string = VERSION || "unknown"; -const masterNode: string = HA_MASTER === "true" ? "✓" : "✗"; -const unsafeSync: string = HA_UNSAFE === "true" ? "✓" : "✗"; - -let trustedProxies: string = ""; - -if (TRUSTED_PROXIES) { - trustedProxies = TRUSTED_PROXIES; -} else { - trustedProxies = "✗"; -} - -function writeUserConf(port: number) { - let previousConfig = null; - let shouldRewriteConfig = false; - - const installationDetails = { - installedAt: new Date().toISOString(), - backendVersion: version, - inDocker: inDocker, - installedBy: os.userInfo().username, - platform: os.platform(), - arch: os.arch(), - }; - - if (fs.existsSync(userConf)) { - try { - previousConfig = JSON.parse(fs.readFileSync(userConf, "utf-8")); - if (previousConfig.backendVersion !== version) { - shouldRewriteConfig = true; - logger.debug( - "Version change detected. Rewriting configuration file...", - ); - } else { - logger.debug("No version change detected. Skipping re-initialization."); - } - } catch (error) { - logger.error( - "Error reading the configuration file. Rewriting it...", - error, - ); - shouldRewriteConfig = true; - } - } else { - logger.debug("Configuration file not found. Creating a new one..."); - shouldRewriteConfig = true; - } - - if (shouldRewriteConfig) { - atomicWrite(userConf, JSON.stringify(installationDetails, null, 2)); - logger.debug("Configuration file created/updated:", userConf); - } - - const startDetails = { - startedAt: new Date().toISOString(), - backendVersion: version, - }; - - logger.info("-----------------------------------------"); - logger.info(`Starting at : ${startDetails.startedAt}`); - logger.info(`Running env : ${process.env.NODE_ENV}`); - logger.info(`Version : ${startDetails.backendVersion}`); - logger.info(`Docker : ${installationDetails.inDocker}`); - logger.info(`Running as : ${installationDetails.installedBy}`); - logger.info(`Platform : ${installationDetails.platform}`); - logger.info(`Arch : ${installationDetails.arch}`); - logger.info(`Master node : ${masterNode}`); - logger.info(`Unsafe sync : ${unsafeSync}`); - logger.info(`Proxies : ${trustedProxies}`); - logger.info(`Log Level : ${LOG_LEVEL}`); - logger.info(`Server : http://localhost:${port}`); - if (process.env.NODE_ENV !== "production") { - logger.info(`Swagger-UI : http://localhost:${port}/api-docs`); - } - logger.info("-----------------------------------------"); -} - -export default writeUserConf; diff --git a/src/config/initFiles.ts b/src/config/initFiles.ts deleted file mode 100644 index 7524907..0000000 --- a/src/config/initFiles.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { existsSync } from "fs"; -import logger from "../utils/logger"; -import { atomicWrite } from "../utils/atomicWrite"; - -const files = [ - { path: "./src/data/highAvailability.json", content: "{}" }, - { - path: "./src/data/password.json", - content: JSON.stringify( - { - hash: "", - salt: "", - }, - null, - 2, - ), - }, - { path: "./src/data/states.json", content: "{}" }, - { - path: "./src/data/template.json", - content: JSON.stringify( - { text: "{{name}} is {{state}} on {{hostName}}" }, - null, - 2, - ), - }, - { path: "./src/data/frontendConfiguration.json", content: "[]" }, - { path: "./src/data/usePassword.txt", content: "false" }, -]; - -function initFiles(): void { - files.forEach(({ path: filePath, content }) => { - if (!existsSync(filePath)) { - atomicWrite(filePath, content); - logger.info(`Created: ${filePath}`); - } else { - logger.debug(`Skipped (already exists): ${filePath}`); - } - }); -} - -export default initFiles; diff --git a/src/config/stacks.ts b/src/config/stacks.ts deleted file mode 100644 index def75dc..0000000 --- a/src/config/stacks.ts +++ /dev/null @@ -1,260 +0,0 @@ -import logger from "../utils/logger"; -import fs from "fs"; -import path from "path"; -import YAML from "yamljs"; -import { DockerComposeFile } from "../typings/dockerCompose"; -import { dockerStackProperty, dockerStackEnv } from "../typings/dockerStackEnv"; -import { stackConfig } from "../typings/stackConfig"; -import { validate } from "../handlers/stack"; -import { atomicWrite } from "../utils/atomicWrite"; -import { AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT } from "./variables"; - -const nameRegex = /^[A-Za-z0-9_-]+$/; -const stackRootFolder = "./stacks"; -const configFilePath = `${stackRootFolder}/.config.json`; - -async function getStackCompose(name: string) { - try { - await validate(name); - const stackCompose = `${stackRootFolder}/${name}/docker-compose.yaml`; - - return YAML.parse(fs.readFileSync(stackCompose, "utf-8")); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - throw new Error(errorMsg); - } -} - -async function getStackConfig(): Promise { - try { - return fs.readFileSync(configFilePath, "utf-8"); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - throw new Error(errorMsg); - } -} - -async function createStack( - name: string, - content: DockerComposeFile, - override: boolean, -) { - try { - if (!name) { - const errorMsg = "Name required"; - logger.error(errorMsg); - throw new Error(errorMsg); - } - - if (!nameRegex.test(name)) { - const errorMsg = "Name does not match [A-Za-z0-9_-]"; - logger.error(errorMsg); - throw new Error(errorMsg); - } - - if (!content) { - const errorMsg = "Data for this stack is required"; - logger.error(errorMsg); - throw new Error(errorMsg); - } - - const stackFolderPath = `${stackRootFolder}/${name}`; - - if (!fs.existsSync(stackFolderPath)) { - fs.mkdirSync(stackFolderPath, { recursive: true }); - logger.debug(`Created stack folder at ${stackFolderPath}`); - } - - updateConfigFile(name); - - let yamlContent = ""; - let environmentFileData: dockerStackEnv = { environment: [] }; - if (AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT == "true" && override == false) { - logger.debug("AEFM is activated"); - const { cleanCompose, envSchema } = extractAndRemoveEnv(content); - yamlContent = YAML.stringify(cleanCompose, 10, 2); - environmentFileData = envSchema; - - await writeEnvFile(name, environmentFileData); - } else { - yamlContent = YAML.stringify(content, 10, 2); - } - - const filePath = `${stackFolderPath}/docker-compose.yaml`; - atomicWrite(filePath, yamlContent); - logger.debug(`Stack content written to ${filePath}`); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - throw new Error(errorMsg); - } -} - -function updateConfigFile(stackName: string) { - try { - let config: stackConfig = { stacks: [] }; - if (fs.existsSync(configFilePath)) { - const configData = fs.readFileSync(configFilePath, "utf-8"); - config = JSON.parse(configData); - } - - const stacks = config.stacks || []; - - if (!stacks.includes(stackName)) { - stacks.push(stackName); - } - - const updatedConfig = { stacks }; - atomicWrite(configFilePath, JSON.stringify(updatedConfig, null, 2)); - logger.debug(`Updated .config.json with stack name: ${stackName}`); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(`Error updating .config.json: ${errorMsg}`); - throw new Error(errorMsg); - } -} - -async function writeEnvFile( - name: string, - data: dockerStackEnv, -): Promise { - try { - await validate(name); - - if (!nameRegex.test(name)) { - const sanitizedStackName = name.replace(/\n|\r/g, ""); - const errorMsg = `Invalid stack name: ${sanitizedStackName}`; - logger.error(errorMsg); - return false; - } - - const dockerEnvPath = path.resolve(stackRootFolder, name, "docker.env"); - const dockerEnvPathBak = path.resolve( - stackRootFolder, - name, - ".docker.env.bak", - ); - - if ( - !dockerEnvPath.startsWith(path.resolve(stackRootFolder)) || - !dockerEnvPathBak.startsWith(path.resolve(stackRootFolder)) - ) { - const sanitizedStackName = name.replace(/\n|\r/g, ""); - const errorMsg = `Path traversal attempt detected: ${sanitizedStackName}`; - logger.error(errorMsg); - return false; - } - - const variableNames = data.environment.map(({ name }) => name); - const duplicateVars = variableNames.filter( - (item, index) => variableNames.indexOf(item) !== index, - ); - - if (duplicateVars.length > 0) { - const duplicatesList = duplicateVars.join(", "); - const sanitizedDuplicatesList = duplicatesList.replace(/\n|\r/g, ""); - const errorMsg = `Duplicate environment variables detected: ${sanitizedDuplicatesList}`; - logger.error(errorMsg); - return false; - } - - const envFileContent = data.environment - .map(({ name, value }) => `${name}="${value}"`) - .join("\n"); - - if (fs.existsSync(dockerEnvPath)) { - logger.debug("Creating a local backup"); - const previousData = fs.readFileSync(dockerEnvPath); - atomicWrite(dockerEnvPathBak, previousData); - } - - atomicWrite(dockerEnvPath, envFileContent); - return true; - } catch (error: unknown) { - const errorMsg = ( - error instanceof Error ? error.message : String(error) - ).replace(/\n|\r/g, ""); - logger.error(errorMsg); - throw new Error(errorMsg); - } -} - -async function getEnvFile(name: string) { - await validate(name); - const dockerEnvPath = path.resolve(stackRootFolder, name, "docker.env"); - if (!dockerEnvPath.startsWith(path.resolve(stackRootFolder))) { - throw new Error("Invalid path"); - } - - if (fs.existsSync(dockerEnvPath)) { - const data = fs.readFileSync(dockerEnvPath, "utf-8"); - - const environment: dockerStackProperty[] = data - .split("\n") - .filter((line) => line.trim() !== "" && line.includes("=")) - .map((line) => { - const [name, ...valueParts] = line.split("="); - const value = valueParts.join("=").replace(/^"|"$/g, ""); - return { name: name.trim(), value: value.trim() }; - }); - - return { environment }; - } else { - return null; - } -} - -function extractAndRemoveEnv(data: DockerComposeFile): { - cleanCompose: DockerComposeFile; - envSchema: dockerStackEnv; -} { - const environment: dockerStackProperty[] = []; - const envCount: Record = {}; - - for (const [, service] of Object.entries(data.services)) { - if (service.environment) { - for (const key of Object.keys(service.environment)) { - envCount[key] = (envCount[key] || 0) + 1; - } - } - } - - for (const [, service] of Object.entries(data.services)) { - if (service.environment) { - const remainingEnvironment: Record = {}; - - for (const [key, value] of Object.entries(service.environment)) { - if (envCount[key] === 1) { - environment.push({ name: key, value }); - } else { - remainingEnvironment[key] = value; - } - } - - service.environment = remainingEnvironment; - - if (Object.keys(service.environment).length === 0) { - delete service.environment; - } - } - - if (!service.env_file) { - service.env_file = ["./docker.env"]; - } - } - - return { - cleanCompose: data, - envSchema: { environment }, - }; -} - -export { - createStack, - getStackConfig, - getStackCompose, - writeEnvFile, - getEnvFile, -}; diff --git a/src/config/swagger.yaml b/src/config/swagger.yaml deleted file mode 100644 index 2230f73..0000000 --- a/src/config/swagger.yaml +++ /dev/null @@ -1,2084 +0,0 @@ -openapi: "3.0.0" - -security: - - passwordAuth: [] - -info: - title: "DockStatAPI" - version: "2.0.1" - externalDocs: - description: DockStat(API) Wiki - url: https://outline.itsnik.de/s/dockstat - license: - name: BSD-3-Clause - url: https://github.com/Its4Nik/dockstatapi/tree/main?tab=BSD-3-Clause-1-ov-file#readme - contact: - email: info@itsnik.de - description: |- - ![DockStat](https://github.com/Its4Nik/dockstatapi/blob/dev/.github/DockStat-dark.png?raw=true) - - # Pipelines - - [![Docker Image CI](https://img.shields.io/github/actions/workflow/status/Its4Nik/dockstatapi/build-image.yml?branch=main&label=Docker%20Image%20CI&style=for-the-badge&logo=docker)](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-image.yml) - [![Validation](https://img.shields.io/github/actions/workflow/status/Its4Nik/dockstatapi/validation.yml?branch=dev&label=Validation&style=for-the-badge&logo=checkmarx)](https://github.com/Its4Nik/dockstatapi/actions/workflows/validation.yml) - - # Feature List: - - - Swagger API Documentation - - Database (Keeps data for 24 hours max) - - Advanced authentication using hashes and salt - - `http` API to configure the backend - - Multi-arch docker builds (using buildx github action) - - Advanced security through middlewares: rate-limiting and authentication - - Multi Arch Docker builds through docker buildx - - High Availability using single master and unlimited worker nodes! - - # 🔗 DockStatAPI v2 Documentation - - _⚠️ = Deprecation warning_ - - - [Introduction](https://outline.itsnik.de/s/dockstat) - - - [DockstatAPI v2](https://outline.itsnik.de/s/dockstat/doc/dockstatapi-v2-XRMDKRqMIg) - - - [API reference](https://outline.itsnik.de/s/dockstat/doc/api-reference-1PTxqx1MQ6) - - [How dependency graphs are made](https://outline.itsnik.de/s/dockstat/doc/how-the-dependecy-graphs-are-made-svuZbEHH9g) - - - [DockStat v1](https://outline.itsnik.de/s/dockstat/doc/dockstat-v1-zVaFS4zROI) - - - [⚠️ Customisation](https://outline.itsnik.de/s/dockstat/doc/customization-PiBz4OpQIZ) - - [⚠️ Themes](https://outline.itsnik.de/s/dockstat/doc/themes-BFhN6ZBbYx) - - [⚠️ Installation](https://outline.itsnik.de/s/dockstat/doc/installation-DaO99bB86q) - - - [⚠️ DockStatAPI v1](https://outline.itsnik.de/s/dockstat/doc/dockstatapi-v1-jLcVCfPNmS) - - [⚠️ Integrations](https://outline.itsnik.de/s/dockstat/doc/integrations-Agq1oL6HxF) - - [⚠️ Backend API reference](https://outline.itsnik.de/s/dockstat/doc/backend-api-reference-YzcBbDvY33) - -tags: - - name: Authentication - description: Routes to setup / configure authentication - - - name: Configuration - description: Configuring the backend - - - name: Database queries - description: Queries made against the SQLite database - - - name: "Frontend Configuration" - description: Backend routes to configure the integrated "frontend service" - - - name: Miscellaneous - description: Some "random" routes which still can be useful - - - name: High availability - description: High availability routes, mainly used by HA sync - - - name: Notification Service - description: Routes to configure the notification service - - - name: Stacks - description: Management of the Stack module - -servers: - - url: http://localhost:9876 - description: "Your DockStatAPI instance" - -paths: - # ------------------------------ - # Authentication setup: - /auth/enable: - post: - tags: - - "Authentication" - summary: Enable authentication for every route - operationId: enableAuth - parameters: - - name: password - in: query - required: true - explode: true - schema: - type: string - default: super-secret - responses: - "200": - description: Success - Successfully enabled authentication - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "success" - message: - type: string - example: "Authentication enabled successfully" - - "403": - description: Error - Password is required / Authentication is already enabled - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /auth/disable: - post: - tags: - - "Authentication" - summary: Disable authentication for every route - operationId: disableAuth - parameters: - - name: password - in: query - required: true - explode: true - schema: - type: string - default: super-secret - responses: - "200": - description: Succes - Succesfully disabled authentication - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "success" - message: - type: string - example: "Authentication disabled successfully" - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - # ------------------------------ - # Database queries: - /data/latest: - get: - tags: - - "Database queries" - summary: Fetched the last added entry from the Database and provides it via a JSON output - operationId: getLatestData - responses: - "200": - description: Succes - Successfully fetched the database - content: - application/json: - schema: - $ref: "#/components/schemas/ServerContainers" - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "404": - description: Error - No entries found inside database - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /data/all: - get: - tags: - - "Database queries" - summary: Provides all database entries with an index starting from 0 - operationId: getAllData - responses: - "200": - description: Succes - Successfully fetched the database - content: - application/json: - schema: - $ref: "#/components/schemas/IndexedServerContainers" - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "404": - description: Error - No entries found inside database - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /data/clear: - delete: - tags: - - "Database queries" - summary: Deletes all database entries - operationId: dataClear - responses: - "200": - description: Succes - Successfully cleared the database - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "success" - message: - type: string - example: "Successfully cleared the database" - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - # ------------------------------ - # Configuration: - /api/hosts: - get: - tags: - - "Configuration" - summary: Retrieves the configured name of all added Hosts - operationId: getHosts - responses: - "200": - description: Succes - Successfully fetched all configured hosts - content: - application/json: - schema: - type: array - example: '[ "Host-1", "Host-2" ]' - - "400": - description: Error - No hosts defined, please add a host via /conf/addHost - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /api/host/{hostName}/stats: - get: - tags: - - "Configuration" - summary: Shows general information about the target host, like dockeer engine version - operationId: getHostInfo - parameters: - - name: hostName - in: path - description: Hostname of the target host - required: true - schema: - type: string - responses: - "200": - description: Succes - Successfully fetched info about target host - content: - application/json: - schema: - $ref: "#/components/schemas/HostInfo" - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "404": - description: Error - No Host found - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /api/system: - get: - tags: - - "Configuration" - summary: Fetched the installation details of this DockStatAPI instance - operationId: getSystem - responses: - "200": - description: Succes - Fetched system configuration - content: - application/json: - schema: - type: object - properties: - installedAt: - type: string - format: date-time - example: "2024-12-25T19:20:02.418Z" - backendVersion: - type: string - example: "2.0.1" - inDocker: - type: boolean - example: false - installedBy: - type: string - example: "user" - platform: - type: string - example: "linux" - arch: - type: string - example: "x64" - "400": - description: Error - Received empty configuration - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /api/config: - get: - tags: - - "Configuration" - summary: Retrieves information about the configured hosts - operationId: getConfig - responses: - "200": - description: Succes - Fetched system configuration - content: - application/json: - schema: - type: object - properties: - hosts: - type: array - items: - type: object - properties: - name: - type: string - example: "Host-1" - url: - type: string - example: "192.168.2.12" - port: - type: string - example: "2375" - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /api/frontend-config: - get: - tags: - - "Configuration" - summary: Fetches the "Frontend Configuration" => Used in the DockStat frontend - operationId: getFrontendConfig - responses: - "200": - description: Succes - Fetched "Frontend Configuration" - content: - application/json: - schema: - $ref: "#/components/schemas/FrontendConfig" - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /api/current-schedule: - get: - tags: - - "Configuration" - summary: Shows the current configured schedule (for fetching data) in seconds - operationId: getSchedule - responses: - "200": - description: Succes - Fetched schedule - content: - application/json: - schema: - type: object - properties: - interval: - type: integer - example: 600 - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /api/status: - get: - tags: - - "Miscellaneous" - summary: Pings all hosts to check reachability - operationId: getStatus - responses: - "200": - description: Succes - Gathered Status - content: - application/json: - schema: - $ref: "#/components/schemas/ApiStatus" - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /api/containers: - get: - tags: - - "Miscellaneous" - summary: Fetched all container data directly from the host without reading from the database - operationId: getContainers - responses: - "200": - description: Succes - Fetched all container statistics - content: - application/json: - schema: - $ref: "#/components/schemas/ServerContainers" - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - # ------------------------------ - # High availability: - /ha/config: - get: - tags: - - "High availability" - summary: Get the current high availability config - operationId: getHaConfig - responses: - "200": - description: Succes - Fetched high availability config - content: - application/json: - schema: - $ref: "#/components/schemas/HaConfig" - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - - /ha/sync: - post: - tags: - - "High availability" - deprecated: true - summary: This route is not deprecated, but only used by the high availability feature - operationId: syncHa - responses: - "200": - description: Succes - Synchronized successfully - "400": - description: Error - `files` object is missing or invalid - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - - /ha/prepare-sync: - get: - tags: - - "High availability" - deprecated: true - summary: This route is not deprecated, but only used by the high availability feature - operationId: syncPrepare - responses: - "200": - description: Succes - Prepared all files for syncing - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - - # ------------------------------ - # Notification Service: - /notification-service/get-template: - get: - tags: - - "Notification Service" - summary: Fetches the current template for the notification service - operationId: getNsTemplate - responses: - "200": - description: Success - Fetched notification template - content: - application/json: - schema: - $ref: "#/components/schemas/Notification-Template" - "400": - description: Error - Error while reading file (see server logs) - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /notification-service/set-template: - post: - tags: - - "Notification Service" - - "Configuration" - summary: Update the current notification template - operationId: setNsTemplate - requestBody: - required: true - content: - application/json: - schema: - $ref: "#/components/schemas/Notification-Template" - responses: - "200": - description: Success - Template updated successfully - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "success" - message: - type: string - example: "Template updated successfully." - "400": - description: Error - Invalid input format. Expected JSON with a 'text' field - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "error" - message: - type: string - example: "Invalid input format. Expected JSON with a 'text' field" - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /notification-service/test/{type}/{containerId}: - post: - tags: - - "Notification Service" - summary: Test a specific type of notification using real data - operationId: testNs - parameters: - - in: path - name: type - required: true - schema: - type: string - description: The desired notification to test - - - in: path - name: containerId - required: true - schema: - type: string - description: A real container ID is needed to test templating functionality - responses: - "200": - description: Success - Sent test notification - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "success" - message: - type: string - example: "Sent test notification" - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - # ------------------------------ - # Configuration: - /conf/addHost: - put: - tags: - - "Configuration" - summary: Adds a new host to the configuration and starts querying it - operationId: addHost - parameters: - - name: name - in: query - required: true - description: A name for the new host - - name: url - in: query - required: true - description: The target IP or dns entry - - name: port - in: query - required: true - description: The targets port on which Docker-Socket-Proxy runs - responses: - "200": - description: Success - Host added successfully - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "success" - message: - type: string - example: "Host added successfully" - "400": - description: Error - Name, Port, and URL are required - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "error" - message: - type: string - example: "Name, Port, and URL are required" - "401": - description: Host already exists - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "error" - message: - type: string - example: "Host already exists" - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /conf/removeHost: - delete: - tags: - - "Configuration" - summary: Removes an host from the config - operationId: removeHost - parameters: - - name: hostName - in: query - required: true - description: "The name of the to-be-removed-Host" - responses: - "200": - description: Success - Host removed successfully - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "success" - message: - type: string - example: "Host removed successfully" - "401": - description: Error - Host name is required - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "error" - message: - type: string - example: "Host name is required" - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "404": - description: Error - Host not found - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "error" - message: - type: string - example: "Host not found" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /conf/scheduler: - tags: - - "Configuration" - summary: Adjust the scheduler timing - operationId: adjustSchedule - parameters: - - name: interval - in: query - required: true - description: "Adjust the schedule timing (in seconds)" - responses: - "200": - description: Success - Timing updated - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "success" - message: - type: string - example: "Updated interval" - "401": - description: Error - Interval must be between 5 minutes and 6 hours - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "error" - message: - type: string - example: "Interval must be between 5 minutes and 6 hours." - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - # ------------------------------ - # Frontend routes: - /frontend/show/{containerName}: - post: - tags: - - "Frontend Configuration" - operationId: frShowCon - summary: Set `hide` to false for the specified container - parameters: - - name: containerName - in: path - schema: - type: string - required: true - description: The name of the container to unhide - responses: - "200": - description: Success - now showing the container - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "success" - message: - type: string - example: "Container unhidden successfully." - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /frontend/hide/{containerName}: - delete: - tags: - - "Frontend Configuration" - operationId: frHideCon - summary: Set `hide` to true for the specified container - parameters: - - name: containerName - in: path - schema: - type: string - required: true - description: The name of the container to unhide - responses: - "200": - description: Success - now hiding the container - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "success" - message: - type: string - example: "Hid container succesfully" - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /frontend/tag/{containerName}/{tag}: - post: - tags: - - "Frontend Configuration" - operationId: frTagCon - summary: Add a tag to the tag array for the specified container - parameters: - - name: containerName - in: path - schema: - type: string - required: true - description: The name of the container to add a tag to - - name: tag - in: path - schema: - type: string - required: true - description: The name of the tag to add - responses: - "200": - description: Success - Tag added successfully - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "success" - message: - type: string - example: "Tag added successfully." - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /frontend/remove-tag/{containerName}/{tag}: - delete: - tags: - - "Frontend Configuration" - operationId: frRmTagCon - summary: Remove the specified tag from the tag array for the specified container - parameters: - - name: containerName - in: path - schema: - type: string - required: true - description: The name of the container to remove a tag from - - name: tag - in: path - schema: - type: string - required: true - description: The name of the tag to remove - responses: - "200": - description: Success - Tag removed successfully - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "success" - message: - type: string - example: "Tag removed successfully." - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /frontend/pin/{containerName}: - post: - tags: - - "Frontend Configuration" - operationId: frPinCon - summary: Set `pinned` to true for the specified container - parameters: - - name: containerName - in: path - schema: - type: string - required: true - description: The name of the container to pin - responses: - "200": - description: Success - Container pinned successfully - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "success" - message: - type: string - example: "Container pinned successfully." - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /frontend/unpin/{containerName}: - delete: - tags: - - "Frontend Configuration" - operationId: frRmPinCon - summary: Set `pinned` to false for the specified container - parameters: - - name: containerName - in: path - schema: - type: string - required: true - description: The name of the container to unpin - responses: - "200": - description: Success - Container unpinned successfully - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "success" - message: - type: string - example: "Container unpinned successfully." - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /frontend/add-link/{containerName}/{link}: - post: - tags: - - "Frontend Configuration" - operationId: frAddLinkCon - summary: Add a link to the specified container - parameters: - - name: containerName - in: path - schema: - type: string - required: true - description: The name of the container to add a link to - - name: link - in: path - schema: - type: URI - required: true - allowReserved: false - description: The URI of the link (please use Uniform Resource Identifier format) - responses: - "200": - description: Success - Link added to container successfully - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "success" - message: - type: string - example: "Link added successfully." - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /frontend/remove-link/{containerName}: - delete: - tags: - - "Frontend Configuration" - operationId: frRmLinkCon - summary: Remove a link to the specified container - parameters: - - name: containerName - in: path - schema: - type: string - required: true - description: The name of the container to remove a link from - responses: - "200": - description: Success - Link removed from container successfully - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "success" - message: - type: string - example: "Link removed successfully." - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /frontend/add-icon/{containerName}/{icon}/{useCustomIcon}: - post: - tags: - - "Frontend Configuration" - operationId: frAddIcon - summary: Add an icon (path) to the specified container - parameters: - - name: containerName - in: path - schema: - type: string - required: true - description: The name of the container to add an icon to - - name: icon - in: path - schema: - type: string - required: true - description: The name of the icon file - - name: useCustomIcon - in: path - schema: - type: boolean - required: false - description: If the icon is a custom icon or not - responses: - "200": - description: Success - Icon added to container successfully - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "success" - message: - type: string - example: "Icon added successfully." - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /frontend/remove-icon/{containerName}: - delete: - tags: - - "Frontend Configuration" - operationId: frRmIcon - summary: Remove an icon from the specified container - parameters: - - name: containerName - in: path - schema: - type: string - required: true - description: The name of the container to remove an icon from - responses: - "200": - description: Success - Icon removed from container successfully - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "success" - message: - type: string - example: "Icon removed successfully." - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - # ------------------------------ - # Stack management - /stacks/create/{name}: - post: - tags: - - "Stacks" - operationId: createStack - summary: Creates a docker-compose file inside the stack name directory - requestBody: - required: true - content: - application/json: - schema: - type: string - description: Your docker-compose.yaml contents - parameters: - - name: name - in: path - schema: - type: string - required: true - description: The name of the stack - responses: - "200": - description: Success - Stack created - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "success" - message: - type: string - example: "Stack created" - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /stacks/start/{name}: - post: - tags: - - "Stacks" - operationId: startStack - summary: Starts the defined stack - parameters: - - name: name - in: path - schema: - type: string - required: true - description: The name of the stack - responses: - "200": - description: Success - Stack started - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "success" - message: - type: string - example: "Stack created" - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /stacks/stop/{name}: - post: - tags: - - "Stacks" - operationId: stopStack - summary: Stops the defined stack - parameters: - - name: name - in: path - schema: - type: string - required: true - description: The name of the stack - responses: - "200": - description: Success - Stack stopped - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "success" - message: - type: string - example: "Stack stopped" - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /stacks/get/{name}: - get: - tags: - - "Stacks" - operationId: getStack - summary: Get the docker-compose.yaml (as JSON) from the defined stack - parameters: - - name: name - in: path - schema: - type: string - required: true - description: The name of the stack - responses: - "200": - description: Success - Stack fetched - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /stacks/set-env/{name}: - post: - tags: - - "Stacks" - operationId: setStackEnv - summary: Set the docker.env (as JSON) from the defined stack - requestBody: - required: true - content: - application/json: - schema: - type: string - description: Your docker.env contents - parameters: - - name: override - in: query - required: false - description: Whether to override (true) the automatic environment file management (boolean value) - - name: name - in: path - schema: - type: string - required: true - description: The name of the stack - responses: - "200": - description: Success - Stack environment set - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /stacks/get-env/{name}: - get: - tags: - - "Stacks" - operationId: getStackEnv - summary: Get the docker.env (as JSON) from the defined stack - parameters: - - name: name - in: path - schema: - type: string - required: true - description: The name of the stack - responses: - "200": - description: Success - Stack config fetched - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - -# ------------------------------ -components: - securitySchemes: - passwordAuth: - type: apiKey - in: header - name: x-password - description: Password required for authentication - - schemas: - Notification-Template: - type: object - properties: - text: - type: string - example: "{{container}} on {{host}} is {{state}}" - - IndexedServerContainers: - type: object - properties: - "0": - type: object - properties: - Host-1: - type: array - items: - $ref: "#/components/schemas/Container" - additionalProperties: false - - ServerContainers: - type: object - properties: - Host-1: - type: array - items: - $ref: "#/components/schemas/Container" - additionalProperties: false - - Container: - type: object - properties: - name: - type: string - description: The name of the container. - example: "Container-1" - id: - type: string - description: The unique identifier of the container. - example: "a84ca83bb0e7f8c24fe472b9164d40a4bae518ece8369e6776f722b81dd65bcf" - hostName: - type: string - description: The hostname of the server. - example: "Host-1" - state: - type: string - description: The current state of the container. - example: "running" - cpu_usage: - type: number - description: The CPU usage of the container in arbitrary units. - example: 625185.1851851852 - mem_usage: - type: integer - description: Memory usage in bytes. - example: 359899136 - mem_limit: - type: integer - description: Memory limit in bytes. - example: 8127893504 - net_rx: - type: integer - description: Total network received in bytes. - example: 11004185462 - net_tx: - type: integer - description: Total network transmitted in bytes. - example: 9950013623 - current_net_rx: - type: integer - description: Current network received in bytes. - example: 11004185462 - current_net_tx: - type: integer - description: Current network transmitted in bytes. - example: 9950013623 - networkMode: - type: string - description: The network mode of the container. - example: "docker_default" - - HostInfo: - type: object - properties: - hostName: - type: string - example: "Host-1" - info: - type: object - properties: - ID: - type: string - format: uuid - example: "32b5fad9-9b12-48b0-9ce7-178f2886ad60" - Containers: - type: integer - example: 8 - ContainersRunning: - type: integer - example: 8 - ContainersPaused: - type: integer - example: 0 - ContainersStopped: - type: integer - example: 0 - Images: - type: integer - example: 7 - OperatingSystem: - type: string - example: "Ubuntu 24.04 LTS" - KernelVersion: - type: string - example: "6.8.0-38-generic" - Architecture: - type: string - example: "x86_64" - MemTotal: - type: integer - example: 8127893504 - NCPU: - type: integer - example: 4 - version: - type: object - properties: - Components: - type: object - properties: - Engine: - type: string - example: "27.1.1" - containerd: - type: string - example: "1.7.19" - runc: - type: string - example: "1.7.19" - docker-init: - type: string - example: "0.19.0" - - Frontend: - type: object - properties: - name: - type: string - description: The name of the container - hidden: - type: boolean - description: Whether the container is hidden - tags: - type: array - items: - type: string - description: List of tags associated with the container - link: - type: string - format: uri - description: A link associated with the container - icon: - type: string - description: Icon for the container - pinned: - type: boolean - description: Whether the container is pinned - required: - - name - - FrontendConfig: - type: array - items: - $ref: "#/components/schemas/Frontend" - - ApiStatus: - type: object - properties: - ApiReachable: - type: boolean - description: Whether the API is reachable - online: - type: object - description: Status of individual services keyed by their names - properties: - Host-1: - type: boolean - Host-2: - type: boolean - required: - - ApiReachable - - online - - HaConfig: - type: object - properties: - active: - type: boolean - description: Whether High availability is active or nots - master: - type: boolean - description: Whether this node is the master node - nodes: - type: array - items: - type: string - format: hostname - description: List of nodes in the cluster, specified by hostname or IP with port - required: - - active - - master - - nodes - - 401: - type: object - properties: - status: - type: string - example: "error" - message: - type: string - example: "Invalid password" - - 403: - type: object - properties: - status: - type: string - example: "denied" - message: - type: string - example: "Password required" - - 500: - type: object - properties: - status: - type: string - example: "critical" - message: - type: string - example: "Please see the server logs for more info" - - 503: - type: object - properties: - status: - type: string - example: "error" - message: - type: string - example: "Service unavailable. The high-availability lock is currently active. Please try again later." diff --git a/src/config/swaggerConfig.ts b/src/config/swaggerConfig.ts deleted file mode 100644 index 39c074a..0000000 --- a/src/config/swaggerConfig.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { SwaggerOptions } from "swagger-ui-express"; -import { css } from "./swaggerTheme"; - -export const options: SwaggerOptions = { - swaggerOptions: { - tryItOutEnabled: true, - }, - customCss: css, - explorer: false, -}; diff --git a/src/config/swaggerTheme.ts b/src/config/swaggerTheme.ts deleted file mode 100644 index d8a879c..0000000 --- a/src/config/swaggerTheme.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const css = ` - -.swagger-ui .topbar { - display: none -} -`; diff --git a/src/config/variables.ts b/src/config/variables.ts deleted file mode 100644 index 37c67a2..0000000 --- a/src/config/variables.ts +++ /dev/null @@ -1,26 +0,0 @@ -import vars from "../data/variables.json"; - -export const { - VERSION, - RUNNING_IN_DOCKER, - TRUSTED_PROXIES, - HA_MASTER, - HA_MASTER_IP, - HA_NODE, - HA_UNSAFE, - DISCORD_WEBHOOK_URL, - EMAIL_SENDER, - EMAIL_RECIPIENT, - EMAIL_PASSWORD, - EMAIL_SERVICE, - PUSHBULLET_ACCESS_TOKEN, - PUSHOVER_USER_KEY, - PUSHOVER_API_TOKEN, - SLACK_WEBHOOK_URL, - TELEGRAM_BOT_TOKEN, - TELEGRAM_CHAT_ID, - WHATSAPP_API_URL, - WHATSAPP_RECIPIENT, - AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT, - LOG_LEVEL, -} = vars; diff --git a/src/controllers/auth.ts b/src/controllers/auth.ts deleted file mode 100644 index 905e39c..0000000 --- a/src/controllers/auth.ts +++ /dev/null @@ -1,64 +0,0 @@ -import fs from "fs/promises"; -import logger from "../utils/logger"; -const passwordFile: string = "./src/data/password.json"; -const passwordBool: string = "./src/data/usePassword.txt"; - -async function authEnabled(): Promise { - let isAuthEnabled: boolean = false; - let data: string = ""; - try { - data = await fs.readFile(passwordBool, "utf8"); - isAuthEnabled = data.trim() === "true"; - return isAuthEnabled; - } catch (error: unknown) { - logger.error("Error reading file: ", error as Error); - return isAuthEnabled; - } -} - -async function readPasswordFile() { - let data: string = ""; - try { - data = await fs.readFile(passwordFile, "utf8"); - return data; - } catch (error: unknown) { - logger.error("Could not read saved password: ", error as Error); - return data; - } -} - -async function writePasswordFile(passwordData: string) { - try { - await fs.writeFile(passwordFile, passwordData); - setTrue(); - logger.debug("Authentication enabled"); - return "Authentication enabled"; - } catch (error: unknown) { - logger.error("Error writing password file:", error as Error); - return error; - } -} - -async function setTrue() { - try { - await fs.writeFile(passwordBool, "true", "utf8"); - logger.info(`Enabled authentication`); - return; - } catch (error: unknown) { - logger.error("Error writing to the file:", error as Error); - return; - } -} - -async function setFalse() { - try { - await fs.writeFile(passwordBool, "false", "utf8"); - logger.info(`Disabled authentication`); - return; - } catch (error: unknown) { - logger.error("Error writing to the file:", error as Error); - return; - } -} - -export { authEnabled, readPasswordFile, writePasswordFile, setFalse }; diff --git a/src/controllers/containerController.ts b/src/controllers/containerController.ts deleted file mode 100644 index 2883dad..0000000 --- a/src/controllers/containerController.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { getDockerClient } from "../utils/dockerClient"; -import logger from "../utils/logger"; -import { Request, Response } from "express"; -import { createResponseHandler } from "../handlers/response"; - -const getContainers = async (req: Request, res: Response): Promise => { - const ResponseHandler = createResponseHandler(res); - const host: string = (req.query.host as string) || "local"; - - logger.info(`Fetching containers from host: ${host}`); - - try { - const docker = getDockerClient(host); - const containers = await docker.listContainers(); - - return ResponseHandler.rawData( - containers, - `Fetched containers from ${host}`, - ); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } -}; - -const getContainerStats = async ( - containerID: string, - containerHost: string, - res: Response, -): Promise => { - logger.info( - `Fetching stats for container: ${containerID} from host: ${containerHost}`, - ); - const ResponseHandler = createResponseHandler(res); - - try { - const docker = getDockerClient(containerHost); - const container = docker.getContainer(containerID); - const stats = await container.stats({ stream: false }); - - return ResponseHandler.rawData( - stats, - `Successfully fetched stats for container: ${containerID} from host: ${containerHost}`, - ); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } -}; - -export default { - getContainers, - getContainerStats, -}; diff --git a/src/controllers/databaseMigration.ts b/src/controllers/databaseMigration.ts deleted file mode 100644 index 45f88d1..0000000 --- a/src/controllers/databaseMigration.ts +++ /dev/null @@ -1,20 +0,0 @@ -import db from "../config/db"; -import logger from "../utils/logger"; - -function clearOldEntries(): void { - const twentyFourHoursAgo: number = Date.now() - 24 * 60 * 60 * 1000; - - db.run( - `DELETE FROM data WHERE createdAt < ?`, - [twentyFourHoursAgo], - (err: Error | null) => { - if (err) { - logger.error("Error deleting old entries:", err.message); - throw new Error("Database cleanup failed"); - } - logger.info("Old entries cleared successfully"); - }, - ); -} - -export default clearOldEntries; diff --git a/src/controllers/fetchData.ts b/src/controllers/fetchData.ts deleted file mode 100644 index 06e52a9..0000000 --- a/src/controllers/fetchData.ts +++ /dev/null @@ -1,76 +0,0 @@ -import db from "../config/db"; -import { fetchAllContainers } from "../utils/containerService"; -import logger from "../utils/logger"; -import fs from "fs"; -import { atomicWrite } from "../utils/atomicWrite"; -const filePath = "./src/data/states.json"; - -let previousState: { [key: string]: string } = {}; - -interface Container { - name: string; - id: string; - state: string; - hostName: string; -} - -interface AllContainerData { - [host: string]: Container[] | { error: string }; -} - -const fetchData = async (): Promise => { - try { - const allContainerData: AllContainerData = - (await fetchAllContainers()) || {}; - - db.run( - `INSERT INTO data (info) VALUES (?)`, - [JSON.stringify(allContainerData)], - function (error) { - if (error) { - logger.error("Error inserting data:", error); - return; - } - logger.info(`Data inserted with ID: ${this.lastID}`); - }, - ); - - const containerStatus: AllContainerData = {}; - - Object.keys(allContainerData).forEach((host) => { - const containers = allContainerData[host]; - - // Handle if the containers are an array, otherwise handle the error - if (Array.isArray(containers)) { - containerStatus[host] = containers.map((container: Container) => ({ - name: container.name, - id: container.id, - state: container.state, - hostName: container.hostName, - })); - } else { - // If there's an error, handle it separately - containerStatus[host] = { error: "Error fetching containers" }; - } - }); - - if (fs.existsSync(filePath)) { - const fileData = fs.readFileSync(filePath, "utf8"); - previousState = fileData ? JSON.parse(fileData) : {}; - } - - // Compare previous and current state - if (JSON.stringify(previousState) !== JSON.stringify(containerStatus)) { - atomicWrite(filePath, JSON.stringify(containerStatus, null, 2)); - logger.info(`Container states saved to ${filePath}`); - // TODO: Add logic + notification levels per service - } else { - logger.info("No state change detected, notifications not triggered."); - } - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } -}; - -export default fetchData; diff --git a/src/controllers/frontendConfiguration.ts b/src/controllers/frontendConfiguration.ts deleted file mode 100644 index ed4e59d..0000000 --- a/src/controllers/frontendConfiguration.ts +++ /dev/null @@ -1,297 +0,0 @@ -import fs from "fs"; -import logger from "../utils/logger"; -const dataPath: string = "./src/data/frontendConfiguration.json"; -const expression: string = - "https?://(www.)?[-a-zA-Z0-9@:%._+~#=]{1,256}.[a-zA-Z0-9()]{1,6}([-a-zA-Z0-9()@:%_+.~#?&//=]*)"; -const regex = new RegExp(expression); -import { FrontendConfig } from "../typings/frontendConfig"; - -/////////////////////////////////////////////////////////////// -// Hide Containers: -async function hideContainer(containerName: string) { - try { - const data = await readData(); - const containerIndex = data.findIndex( - (container) => container.name === containerName, - ); - - if (containerIndex !== -1) { - data[containerIndex].hidden = true; - await saveData(data); - } else { - data.push({ name: containerName, hidden: true }); - await saveData(data); - } - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } -} - -async function unhideContainer(containerName: string) { - try { - const data = await readData(); - const containerIndex = data.findIndex( - (container) => container.name === containerName, - ); - - if (containerIndex !== -1) { - delete data[containerIndex].hidden; - await saveData(data); - cleanupData(); - } - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } -} - -/////////////////////////////////////////////////////////////// -// Tag containers -async function addTagToContainer(containerName: string, tag: string) { - try { - const data = await readData(); - const containerIndex = data.findIndex( - (container) => container.name === containerName, - ); - - if (containerIndex !== -1) { - if (!data[containerIndex].tags) { - data[containerIndex].tags = []; - } - data[containerIndex].tags.push(tag); - await saveData(data); - } else { - data.push({ name: containerName, tags: [tag] }); - await saveData(data); - } - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } -} - -async function removeTagFromContainer(containerName: string, tag: string) { - try { - const data = await readData(); - const containerIndex = data.findIndex( - (container) => container.name === containerName, - ); - - if (containerIndex !== -1 && data[containerIndex].tags) { - data[containerIndex].tags = data[containerIndex].tags.filter( - (t) => t !== tag, - ); - await saveData(data); - cleanupData(); - } - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } -} - -/////////////////////////////////////////////////////////////// -// Pin containers -async function pinContainer(containerName: string) { - try { - const data = await readData(); - const containerIndex = data.findIndex( - (container) => container.name === containerName, - ); - - if (containerIndex !== -1) { - data[containerIndex].pinned = true; - await saveData(data); - } else { - data.push({ name: containerName, pinned: true }); - await saveData(data); - } - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } -} - -async function unpinContainer(containerName: string) { - try { - const data = await readData(); - const containerIndex = data.findIndex( - (container) => container.name === containerName, - ); - - if (containerIndex !== -1) { - delete data[containerIndex].pinned; - await saveData(data); - cleanupData(); - } - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } -} - -/////////////////////////////////////////////////////////////// -// Add/remove link from containers -async function setLink(containerName: string, link: string) { - if (link.match(regex)) { - try { - const data = await readData(); - const containerIndex = data.findIndex( - (container) => container.name === containerName, - ); - - if (containerIndex !== -1) { - data[containerIndex].link = `${link}`; - await saveData(data); - } else { - data.push({ name: containerName, link: `${link}` }); - await saveData(data); - } - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } - } else { - logger.error(`Provided link is not valid: ${link}`); - throw new Error(`Provided link is not valid: ${link}`); - } -} - -async function removeLink(containerName: string) { - try { - const data = await readData(); - const containerIndex = data.findIndex( - (container) => container.name === containerName, - ); - - if (containerIndex !== -1) { - delete data[containerIndex].link; - await saveData(data); - cleanupData(); - } - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } -} - -/////////////////////////////////////////////////////////////// -// Add/remove icon from containers -async function setIcon(containerName: string, icon: string, custom: boolean) { - try { - const data = await readData(); - const containerIndex: number = data.findIndex( - (container) => container.name === containerName, - ); - - if (custom === true) { - if (containerIndex !== -1) { - data[containerIndex].icon = `custom/${icon}`; - await saveData(data); - } else { - data.push({ name: containerName, icon: `custom/${icon}` }); - await saveData(data); - } - } else if (containerIndex !== -1) { - data[containerIndex].icon = `${icon}`; - await saveData(data); - } else { - data.push({ name: containerName, icon: `${icon}` }); - await saveData(data); - } - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } -} - -async function removeIcon(containerName: string) { - try { - const data = await readData(); - const containerIndex = data.findIndex( - (container) => container.name === containerName, - ); - - if (containerIndex !== -1) { - delete data[containerIndex].icon; - await saveData(data); - cleanupData(); - } - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } -} - -/////////////////////////////////////////////////////////////// -// Data specific functionss -async function readData() { - try { - const data: FrontendConfig = JSON.parse( - await fs.promises.readFile(dataPath, "utf-8"), - ); - return data; - } catch (error: unknown) { - console.error(`Error while reading ${dataPath}: ${error as Error}`); - if (error as Error) { - await saveData([]); - return []; - } else { - throw error; - } - } -} - -async function saveData(data: FrontendConfig) { - try { - await fs.promises.writeFile( - dataPath, - JSON.stringify(data, null, 2), - "utf-8", - ); - logger.info("Succesfully wrote to file"); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } -} - -async function cleanupData() { - try { - const data = await readData(); - let cleanedData: FrontendConfig = []; - - if (data && Array.isArray(data)) { - cleanedData = data.filter((container) => { - // Filter out containers with empty "tags" or containers with only one property (name) - if ( - container.tags && - Array.isArray(container.tags) && - container.tags.length === 0 - ) { - delete container.tags; - } - return Object.keys(container).length > 1; - }); - } - - await saveData(cleanedData); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } -} - -export { - hideContainer, - unhideContainer, - addTagToContainer, - removeTagFromContainer, - pinContainer, - unpinContainer, - setLink, - removeLink, - setIcon, - removeIcon, -}; diff --git a/src/controllers/highAvailability.ts b/src/controllers/highAvailability.ts deleted file mode 100644 index 45db9d7..0000000 --- a/src/controllers/highAvailability.ts +++ /dev/null @@ -1,285 +0,0 @@ -import logger from "../utils/logger"; -import fs from "fs"; -import chokidar from "chokidar"; -import path from "path"; -import { promisify } from "util"; -import { - HA_UNSAFE, - HA_MASTER, - HA_MASTER_IP, - HA_NODE, -} from "../config/variables"; -import { atomicWrite } from "../utils/atomicWrite"; -import { HighAvailabilityConfig, HaNodeConfig, NodeCache } from "../typings/ha"; - -const sleep = promisify(setTimeout); - -const haMasterPath: string = "./src/data/highAvailability.json"; -const haNodePath: string = "./src/data/haNode.json"; -const nodeCachePath: string = "./src/data/nodeCache.json"; -const useUnsafeConnection: boolean = JSON.parse(HA_UNSAFE || "false"); -const lockFilePath: string = "./src/data/ha.lock"; - -const configFiles: string[] = [ - "./src/data/dockerConfig.json", - "./src/data/states.json", - "./src/data/template.json", - "./src/data/frontendConfiguration.json", - "./src/data/nodeCache.json", - "./src/data/usePassword.txt", - "./src/data/password.json", -]; - -const MAX_RETRIES = 10; -const BASE_DELAY_MS = 100; - -async function acquireLock(): Promise { - let retryCount = 0; - - while (fs.existsSync(lockFilePath)) { - if (retryCount >= MAX_RETRIES) { - throw new Error( - "Failed to acquire lock: maximum retry attempts exceeded", - ); - } - - const backoffMs = BASE_DELAY_MS * Math.pow(2, retryCount); - const jitter = Math.random() * 0.3 * backoffMs; - const delayMs = backoffMs + jitter; - - logger.warn( - `Lock file exists, waiting ${Math.round(delayMs)}ms before retry ${retryCount + 1}/${MAX_RETRIES}...`, - ); - await sleep(delayMs); - retryCount++; - } - - try { - atomicWrite(lockFilePath, "locked", { exclusive: true }); - logger.debug("Lock acquired."); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } -} - -async function releaseLock(): Promise { - try { - if (fs.existsSync(lockFilePath)) { - await fs.promises.unlink(lockFilePath); - logger.debug("Lock released."); - } - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } -} - -async function writeConfig( - data: HighAvailabilityConfig | NodeCache | HaNodeConfig, - filePath: string, -): Promise { - await acquireLock(); - try { - logger.debug(`Writing ${filePath}`); - const dirPath: string = path.dirname(filePath); - await fs.promises.mkdir(dirPath, { recursive: true }); - - const jsonData = JSON.stringify(data, null, 2); - await fs.promises.writeFile(filePath, jsonData); - - logger.debug(`${filePath} has been written.`); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } finally { - await releaseLock(); - } -} - -async function readConfig(): Promise { - await acquireLock(); - try { - logger.debug("Reading HA-Config"); - const data: HighAvailabilityConfig = JSON.parse( - fs.readFileSync(haMasterPath, "utf-8"), - ); - return data; - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - return null; - } finally { - await releaseLock(); - } -} - -async function prepareFilesForSync(): Promise> { - const fileData: Record = {}; - try { - for (const filePath of configFiles) { - const content = await fs.promises.readFile(filePath, "utf-8"); - fileData[filePath] = content; - } - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } - return fileData; -} - -async function checkApiReachable(node: string): Promise { - const nodeUrl = - useUnsafeConnection === true - ? `http://${node}/api/status` - : `https://${node}/api/status`; - - logger.info(`Checking node (${nodeUrl}) reachability`); - - try { - const response = await fetch(nodeUrl); - if (!response.ok) { - logger.error(`Failed to reach node ${node}. Status: ${response.status}`); - return false; - } - - const data = await response.json(); - if (data.ApiReachable as boolean) { - logger.info(`Node ${node} is reachable.`); - return true; - } else { - logger.error(`Node ${node} is not reachable. ApiReachable: false`); - return false; - } - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - return false; - } -} - -async function synchronizeFilesWithNodes(): Promise { - const haConfig = await readConfig(); - - if (!haConfig || !haConfig.master || haConfig.nodes.length === 0) { - logger.warn("No slave nodes to synchronize with."); - return; - } - - const files = await prepareFilesForSync(); - - for (const node of haConfig.nodes) { - if (!(await checkApiReachable(node))) { - logger.warn( - `Skipping file sync with ${node} due to connectivity issues.`, - ); - continue; // Skip synchronization if the node is unreachable - } - - const nodeUrl = - useUnsafeConnection === true - ? `http://${node}/ha/sync` - : `https://${node}/ha/sync`; - - logger.info(`Synchronizing files with node: ${node}`); - - const response = await fetch(nodeUrl, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ files }), - }); - - if (response.ok) { - logger.info(`Files synchronized successfully with node: ${node}`); - } else { - logger.error( - `Failed to synchronize files with node ${node}. Status: ${response.status}`, - ); - } - } -} - -function monitorConfigFiles(): void { - const watcher = chokidar.watch(configFiles, { persistent: true }); - - watcher - .on("change", async (filePath) => { - logger.info(`File changed: ${filePath}. Initiating synchronization.`); - await synchronizeFilesWithNodes(); - }) - .on("error", (error) => { - logger.error(`Error watching files: ${(error as Error).message}`); - }); - - logger.info("Started monitoring configuration files for changes."); -} - -async function startMasterNode() { - if (HA_MASTER == "true") { - if (!HA_MASTER_IP) { - logger.error( - "Master's IP is not set, please set the HA_MASTER_IP variable (example: 10.0.0.4:9876)", - ); - } else { - const haNodeConfig: HaNodeConfig = { - master: HA_MASTER_IP, - }; - const haConfig: HighAvailabilityConfig = { - active: true, - master: true, - nodes: HA_NODE ? HA_NODE.split(",").map((node) => node.trim()) : [], - }; - - const nodeCache: NodeCache = HA_NODE - ? HA_NODE.split(",").reduce((cache, node, index) => { - const [ip, port] = node.trim().split(":"); - if (ip && port) { - cache[`node-${index + 1}`] = { ip, port: parseInt(port, 10) }; - } - return cache; - }, {} as NodeCache) - : {}; - - logger.debug("Writing HA-Config(s)"); - await writeConfig(haConfig, haMasterPath); - await writeConfig(haNodeConfig, haNodePath); - await writeConfig(nodeCache, nodeCachePath); - - logger.info("Running startup sync..."); - await synchronizeFilesWithNodes(); - logger.info("Watching config files in ./data"); - monitorConfigFiles(); - } - } else { - logger.info("This is a slave node"); - } -} - -async function ensureFileExists( - filePath: string, - content: string, -): Promise { - await acquireLock(); - try { - const dirPath = path.dirname(filePath); - await fs.promises.mkdir(dirPath, { recursive: true }); - await fs.promises.writeFile(filePath, content, { flag: "w" }); - logger.info(`File updated: ${filePath}`); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } finally { - await releaseLock(); - } -} - -export { - HighAvailabilityConfig, - writeConfig, - readConfig, - prepareFilesForSync, - synchronizeFilesWithNodes, - monitorConfigFiles, - startMasterNode, - ensureFileExists, -}; diff --git a/src/controllers/notificationController.ts b/src/controllers/notificationController.ts deleted file mode 100644 index 0ece955..0000000 --- a/src/controllers/notificationController.ts +++ /dev/null @@ -1,60 +0,0 @@ -import notify from "../utils/notifications/_notify"; -import logger from "../utils/logger"; -import { - DISCORD_WEBHOOK_URL, - EMAIL_SENDER, - EMAIL_RECIPIENT, - EMAIL_PASSWORD, - EMAIL_SERVICE, - PUSHBULLET_ACCESS_TOKEN, - PUSHOVER_USER_KEY, - PUSHOVER_API_TOKEN, - SLACK_WEBHOOK_URL, - TELEGRAM_BOT_TOKEN, - TELEGRAM_CHAT_ID, - WHATSAPP_API_URL, - WHATSAPP_RECIPIENT, -} from "../config/variables"; - -const notificationTypes = { - discord: !!DISCORD_WEBHOOK_URL, - email: !!(EMAIL_SENDER && EMAIL_RECIPIENT && EMAIL_PASSWORD && EMAIL_SERVICE), - pushbullet: !!PUSHBULLET_ACCESS_TOKEN, - pushover: !!(PUSHOVER_API_TOKEN && PUSHOVER_USER_KEY), - slack: !!SLACK_WEBHOOK_URL, - telegram: !!(TELEGRAM_BOT_TOKEN && TELEGRAM_CHAT_ID), - whatsapp: !!(WHATSAPP_API_URL && WHATSAPP_RECIPIENT), -}; - -async function sendNotification(containerId: string) { - if (notificationTypes.discord) { - logger.debug(`Sending notification via discord (${containerId})`); - notify("discord", containerId); - } - if (notificationTypes.email) { - logger.debug(`Sending notification via E-Mail (${containerId})`); - notify("email", containerId); - } - if (notificationTypes.pushbullet) { - logger.debug(`Sending notification via Pushbullet (${containerId})`); - notify("pushbullet", containerId); - } - if (notificationTypes.pushover) { - logger.debug(`Sending notification via Pushover (${containerId})`); - notify("pushover", containerId); - } - if (notificationTypes.slack) { - logger.debug(`Sending notification via Slack (${containerId})`); - notify("slack", containerId); - } - if (notificationTypes.telegram) { - logger.debug(`Sending notification via Telegram (${containerId})`); - notify("slack", containerId); - } - if (notificationTypes.whatsapp) { - logger.debug(`Sending notification via Pushbullet (${containerId})`); - notify("whatsapp", containerId); - } -} - -export default sendNotification; diff --git a/src/controllers/proxy.ts b/src/controllers/proxy.ts deleted file mode 100644 index c091590..0000000 --- a/src/controllers/proxy.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Application } from "express"; -import logger from "../utils/logger"; -import { TRUSTED_PROXIES } from "../config/variables"; - -export default function trustedProxies(app: Application) { - const trusted: string = TRUSTED_PROXIES; - - if (!trusted) { - logger.warn( - "No trusted Proxy configured, if ran behind a proxy please configure it according to the docs", - ); - } else { - app.set("trust proxy", trusted); - } -} diff --git a/src/controllers/scheduler.ts b/src/controllers/scheduler.ts deleted file mode 100644 index db450d9..0000000 --- a/src/controllers/scheduler.ts +++ /dev/null @@ -1,91 +0,0 @@ -import fetchData from "./fetchData"; -import logger from "../utils/logger"; -import db from "../config/db"; -const regex = /(\d{1,5})([smh])/g; - -let fetchInterval = 5 * 60 * 1000; // Fetch data every 5 minutes by default -const cleanupInterval = 24 * 60 * 60 * 1000; // every 24hrs -let intervalId: NodeJS.Timeout; - -const scheduleFetch = () => { - try { - fetchData(); - cleanupOldEntries(); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } - - intervalId = setInterval(() => { - logger.info( - `Fetching data at interval of ${fetchInterval / 1000} seconds.`, - ); - fetchData(); - }, fetchInterval); - - setInterval(() => { - cleanupOldEntries(); - }, cleanupInterval); - - logger.info(`Data fetching scheduled every ${fetchInterval / 1000} seconds.`); - logger.info("Old entries cleanup scheduled every 24 hours."); - - // Additional 20-second interval to log process exit listeners, if any - setInterval(() => { - const exitListeners = process.listeners("exit"); - - if (exitListeners.length > 0) { - logger.info(`Exit listeners detected: ${exitListeners}`); - } - }, 20000); -}; - -const setFetchInterval = (newInterval: number) => { - if (intervalId) { - clearInterval(intervalId); - logger.info("Cleared existing fetch interval."); - } - fetchInterval = newInterval; - scheduleFetch(); - logger.info(`Fetch interval updated to ${fetchInterval / 1000} seconds.`); -}; - -const parseInterval = (interval: string) => { - const timeUnits: { [key: string]: number } = { - s: 1000, - m: 60 * 1000, - h: 60 * 60 * 1000, - }; - - let totalMilliseconds = 0; - let match; - - while ((match = regex.exec(interval))) { - const value = parseInt(match[1], 10); - const unit = match[2]; - totalMilliseconds += value * timeUnits[unit]; - } - - return totalMilliseconds; -}; - -const getCurrentSchedule = () => { - return { - interval: fetchInterval / 1000, - }; -}; - -const cleanupOldEntries = async () => { - const twentyFourHoursAgo = new Date( - Date.now() - 24 * 60 * 60 * 1000, - ).toISOString(); - try { - db.run("DELETE FROM data WHERE timestamp < ?", twentyFourHoursAgo, Error); - logger.info("Old entries cleared from the database."); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } -}; - -export { scheduleFetch, setFetchInterval, parseInterval, getCurrentSchedule }; diff --git a/src/core/database/repository.ts b/src/core/database/repository.ts new file mode 100644 index 0000000..6d5ea55 --- /dev/null +++ b/src/core/database/repository.ts @@ -0,0 +1,74 @@ +import Database from "bun:sqlite"; + +const db = new Database("dockstatapi.db"); + +export const dbFunctions = { + init() { + db.exec(` + CREATE TABLE IF NOT EXISTS docker_hosts ( + id TEXT PRIMARY KEY, + name TEXT, + url TEXT, + poll_interval INTEGER + ); + + CREATE TABLE IF NOT EXISTS container_metrics ( + host_id TEXT, + container_id TEXT, + cpu REAL, + memory REAL, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP + ); + + CREATE TABLE IF NOT EXISTS backend_log_entries ( + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + level TEXT, + message TEXT, + file TEXT, + line NUMBER + ); + `); + }, + + insertMetric(hostId: string, metric: any) { + const stmt = db.prepare(` + INSERT INTO container_metrics (host_id, container_id, cpu, memory) + VALUES (?, ?, ?, ?) + `); + return stmt.run(hostId, metric.containerId, metric.cpu, metric.memory); + }, + + addLogEntry: ( + level: string, + message: string, + file_name: string, + line: number, + ) => { + const stmt = db.prepare(` + INSERT INTO backend_log_entries (level, message, file, line) + VALUES (?, ?, ?, ?) + `); + return stmt.run(level, message, file_name, line); + }, + + getAllLogs() { + const stmt = db.prepare(` + SELECT timestamp, level, message, file, line + FROM backend_log_entries + ORDER BY timestamp DESC + `); + return stmt.all(); + }, + + getLogsByLevel(level: string) { + const stmt = db.prepare(` + SELECT timestamp, level, message, file, line + FROM backend_log_entries + WHERE level = ? + ORDER BY timestamp DESC + `); + return stmt.all(level); + }, +}; + +dbFunctions.init(); diff --git a/src/core/docker/host-manager.ts b/src/core/docker/host-manager.ts new file mode 100644 index 0000000..e2c1ccc --- /dev/null +++ b/src/core/docker/host-manager.ts @@ -0,0 +1,38 @@ +import WebSocket from "ws"; +import { pluginManager } from "~/core/plugins/plugin-manager"; +import { dbFunctions } from "~/core/database/repository"; +import { logger } from "~/core/utils/logger"; + +export class DockerHostManager { + public connections = new Map(); + + async connect(hostId: string, url: string) { + const ws = new WebSocket(url); + + ws.on("open", () => { + this.connections.set(hostId, ws); + logger.info(`Opened connection to ${hostId}`); + }); + + ws.on("message", (data) => { + this.handleData(hostId, JSON.parse(data.toString())); + }); + + ws.on("close", () => { + this.connections.delete(hostId); + logger.info(`Disconnected from Docker host ${hostId}`); + }); + } + + private handleData(hostId: string, data: any) { + dbFunctions.insertMetric(hostId, data); + + if (data.event === "container_start") { + pluginManager.handleContainerStart(data.container); + } + + pluginManager.handleMetrics(data); + } +} + +export const dockerHostManager = new DockerHostManager(); diff --git a/src/core/plugins/loader.ts b/src/core/plugins/loader.ts new file mode 100644 index 0000000..40f79c4 --- /dev/null +++ b/src/core/plugins/loader.ts @@ -0,0 +1,21 @@ +import { pluginManager } from "./plugin-manager"; +import path from "path"; +import fs from "fs"; + +export async function loadPlugins(pluginDir: string) { + const pluginPath = path.join(process.cwd(), pluginDir); + + if (!fs.existsSync(pluginPath)) { + return; + } + + const files = fs.readdirSync(pluginPath); + + for (const file of files) { + if (!file.endsWith(".plugin.ts")) continue; + + const module = await import(path.join(pluginPath, file)); + const plugin = module.default; + pluginManager.register(plugin); + } +} diff --git a/src/core/plugins/plugin-manager.ts b/src/core/plugins/plugin-manager.ts new file mode 100644 index 0000000..15d66f4 --- /dev/null +++ b/src/core/plugins/plugin-manager.ts @@ -0,0 +1,35 @@ +import { EventEmitter } from "events"; + +export interface Plugin { + name: string; + onContainerStart?: (containerInfo: any) => void; + onMetricsReceived?: (metrics: any) => void; + onLogReceived?: (log: string) => void; +} + +export class PluginManager extends EventEmitter { + private plugins: Map = new Map(); + + register(plugin: Plugin) { + this.plugins.set(plugin.name, plugin); + console.log(`Registered plugin: ${plugin.name}`); + } + + unregister(name: string) { + this.plugins.delete(name); + } + + handleContainerStart(containerInfo: any) { + this.plugins.forEach((plugin) => { + plugin.onContainerStart?.(containerInfo); + }); + } + + handleMetrics(metrics: any) { + this.plugins.forEach((plugin) => { + plugin.onMetricsReceived?.(metrics); + }); + } +} + +export const pluginManager = new PluginManager(); diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts new file mode 100644 index 0000000..076e385 --- /dev/null +++ b/src/core/utils/logger.ts @@ -0,0 +1,72 @@ +import { createLogger, format, transports } from "winston"; +import Transport from "winston-transport"; +import path from "path"; +import { dbFunctions } from "../database/repository"; +import chalk from "chalk"; + +const fileLineFormat = format((info) => { + try { + const stack = new Error().stack?.split("\n"); + if (stack) { + for (let i = 2; i < stack.length; i++) { + const line = stack[i].trim(); + if ( + !line.includes("node_modules") && + !line.includes(path.basename(__filename)) + ) { + const matches = line.match(/\(?(.+):(\d+):(\d+)\)?$/); + if (matches) { + info.file = path.basename(matches[1]); + info.line = parseInt(matches[2]); + break; + } + } + } + } + } catch (err) { + // Ignore errors in case stack trace parsing fails + } + return info; +}); + +class SQLiteTransport extends Transport { + constructor(opts?: Transport.TransportStreamOptions) { + super(opts); + } + + log(info: any, callback: () => void) { + const { level, message, file, line } = info; + dbFunctions.addLogEntry(level, message, file || "unknown", line || 0); + callback(); + } +} + +export const logger = createLogger({ + level: "debug", + format: format.combine(fileLineFormat(), format.json()), + transports: [ + new transports.Console({ + format: format.combine( + format.printf(({ level, message, file, line }) => { + const levelColors: { [key: string]: chalk.Chalk } = { + error: chalk.red.bold, + warn: chalk.yellow.bold, + info: chalk.green.bold, + debug: chalk.blue.bold, + verbose: chalk.cyan.bold, + silly: chalk.magenta.bold, + }; + + const paddedLevel = level.padEnd(5).toUpperCase(); + const coloredLevel = (levelColors[level] || chalk.white)(paddedLevel); + + const coloredContext = chalk.cyan(`${file}:${line}`); + const coloredMessage = chalk.gray(message); + + return `[ ${coloredContext.padEnd(22)} ] ${coloredLevel} - ${coloredMessage}`; + }), + ), + }), + new SQLiteTransport(), + ], +}); diff --git a/src/data/frontendConfiguration.json b/src/data/frontendConfiguration.json deleted file mode 100644 index 0637a08..0000000 --- a/src/data/frontendConfiguration.json +++ /dev/null @@ -1 +0,0 @@ -[] \ No newline at end of file diff --git a/src/data/template.json b/src/data/template.json deleted file mode 100644 index 75e12f2..0000000 --- a/src/data/template.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "text": "{{name}} is {{state}} on {{hostName}}" -} diff --git a/src/data/usePassword.txt b/src/data/usePassword.txt deleted file mode 100644 index 02e4a84..0000000 --- a/src/data/usePassword.txt +++ /dev/null @@ -1 +0,0 @@ -false \ No newline at end of file diff --git a/src/handlers/api.ts b/src/handlers/api.ts deleted file mode 100644 index fa7f1f7..0000000 --- a/src/handlers/api.ts +++ /dev/null @@ -1,142 +0,0 @@ -import extractRelevantData from "../utils/extractHostData"; -import { Request, Response } from "express"; -import { getDockerClient } from "../utils/dockerClient"; -import { fetchAllContainers } from "../utils/containerService"; -import { getCurrentSchedule } from "../controllers/scheduler"; -import fs from "fs"; -import checkReachability from "../utils/connectionChecker"; -const configPath = "./src/data/dockerConfig.json"; -const userConf = "./src/data/user.conf"; -import { dockerConfig } from "../typings/dockerConfig"; -import { createResponseHandler } from "./response"; - -class ApiHandler { - private req: Request; - private res: Response; - - constructor(req: Request, res: Response) { - this.req = req; - this.res = res; - } - - hosts() { - const ResponseHandler = createResponseHandler(this.res); - try { - const rawData = fs.readFileSync(configPath, "utf-8"); - const config: dockerConfig = JSON.parse(rawData); - - if (!config.hosts) { - return ResponseHandler.error("No hosts defined in configuration.", 400); - } - - const hosts = config.hosts.map((host) => host.name); - return ResponseHandler.rawData(hosts, "Fetched data for all hosts"); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - system() { - const ResponseHandler = createResponseHandler(this.res); - try { - const rawData = fs.readFileSync(userConf, "utf8"); - const config = JSON.parse(rawData); - - if (!config) { - return ResponseHandler.error("Received empty configuration", 400); - } - - return ResponseHandler.rawData(config, "Fetched system configuration"); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - async hostStats(hostName: string) { - const ResponseHandler = createResponseHandler(this.res); - try { - const docker = getDockerClient(hostName); - const info = await docker.info(); - const version = await docker.version(); - const relevantData = extractRelevantData({ hostName, info, version }); - - if (!relevantData) { - ResponseHandler.error("No host found", 404); - } - - return ResponseHandler.rawData(relevantData, "Fetched Host stats"); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - async containers() { - const ResponseHandler = createResponseHandler(this.res); - try { - const allContainerData = await fetchAllContainers(); - return ResponseHandler.rawData( - allContainerData, - "Fetched all containers across all hosts", - ); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - async config() { - const ResponseHandler = createResponseHandler(this.res); - try { - const rawData = fs.readFileSync(configPath); - const data = JSON.parse(rawData.toString()); - return ResponseHandler.rawData(data, "Fetched config"); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - currentSchedule() { - const ResponseHandler = createResponseHandler(this.res); - try { - const currentSchedule = getCurrentSchedule(); - return ResponseHandler.rawData( - currentSchedule, - "Fetched current schedule", - ); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - async status() { - const ResponseHandler = createResponseHandler(this.res); - try { - const data = await checkReachability(); - return ResponseHandler.rawData(data, "Fetched Status"); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - frontendConfig() { - const configPath: string = "./src/data/frontendConfiguration.json"; - const ResponseHandler = createResponseHandler(this.res); - try { - const rawData = fs.readFileSync(configPath); - const data = JSON.parse(rawData.toString()); - return ResponseHandler.rawData(data, "Fetched frontend configuration"); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } -} - -export const createApiHandler = (req: Request, res: Response) => - new ApiHandler(req, res); diff --git a/src/handlers/auth.ts b/src/handlers/auth.ts deleted file mode 100644 index 4dfbd3f..0000000 --- a/src/handlers/auth.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Request, Response } from "express"; -import { - authEnabled, - readPasswordFile, - writePasswordFile, - setFalse, -} from "../controllers/auth"; -import { createResponseHandler } from "./response"; -import bcrypt from "bcrypt"; - -const saltRounds: number = 10; - -class AuthenticationHandler { - private req: Request; - private res: Response; - - constructor(req: Request, res: Response) { - this.req = req; - this.res = res; - } - - async enable(password: string) { - const ResponseHandler = createResponseHandler(this.res); - try { - if (await authEnabled()) { - return ResponseHandler.denied( - "Password Authentication is already enabled, please deactivate it first", - ); - } - - if (!password) { - return ResponseHandler.denied("Password is required"); - } - - const salt = await bcrypt.genSalt(saltRounds); - const hash = await bcrypt.hash(password, salt); - - const passwordData = { hash, salt }; - writePasswordFile(JSON.stringify(passwordData)); - - return ResponseHandler.ok("Authentication enabled successfully"); - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - async disable(password: string) { - const ResponseHandler = createResponseHandler(this.res); - try { - if (!password) { - return ResponseHandler.denied("Password is required"); - } - - const storedData = JSON.parse(await readPasswordFile()); - const isPasswordValid = await bcrypt.compare(password, storedData.hash); - - if (!isPasswordValid) { - return ResponseHandler.error("Invalid password", 401); - } - - await setFalse(); - return ResponseHandler.ok("Authentication disabled successfully"); - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } -} - -export const createAuthenticationHandler = (req: Request, res: Response) => - new AuthenticationHandler(req, res); diff --git a/src/handlers/conf.ts b/src/handlers/conf.ts deleted file mode 100644 index b49dd2a..0000000 --- a/src/handlers/conf.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { setFetchInterval, parseInterval } from "../controllers/scheduler"; -import { Request, Response } from "express"; -import fs from "fs"; -import { createResponseHandler } from "./response"; -import { target, dockerConfig } from "../typings/dockerConfig"; -const configPath: string = "./src/data/dockerConfig.json"; - -class ConfHandler { - private req: Request; - private res: Response; - - constructor(req: Request, res: Response) { - this.req = req; - this.res = res; - } - - addHost(req: Request) { - const ResponseHandler = createResponseHandler(this.res); - - try { - const { name, url, port } = req.query as unknown as target; - if (!name || !url || !port) { - return ResponseHandler.error("Name, Port, and URL are required.", 400); - } - - const config: dockerConfig = JSON.parse( - fs.readFileSync(configPath, "utf-8"), - ); - - if (config.hosts.some((host) => host.name === name)) { - return ResponseHandler.error("Host already exists.", 422); - } - - config.hosts.push({ name, url, port }); - fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); - - return ResponseHandler.ok("Host added successfully."); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - removeHost(req: Request) { - const ResponseHandler = createResponseHandler(this.res); - try { - const hostName = req.query.hostName as string; - - if (!hostName) { - return ResponseHandler.error("Host name is required.", 401); - } - - const currentState = fs.readFileSync(configPath, "utf-8"); - const config: dockerConfig = JSON.parse(currentState); - - const hostIndex = config.hosts.findIndex( - (host) => host.name === hostName, - ); - - if (hostIndex === -1) { - return ResponseHandler.error("Host not found.", 404); - } - - config.hosts.splice(hostIndex, 1); - - fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); - - return ResponseHandler.ok("Host removed successfully."); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - scheduler(req: Request) { - const ResponseHandler = createResponseHandler(this.res); - try { - const interval = req.query.interval as string; - const newInterval = parseInterval(interval); - - if (newInterval < 5 * 60 * 1000 || newInterval > 6 * 60 * 60 * 1000) { - return ResponseHandler.error( - "Interval must be between 5 minutes and 6 hours.", - 401, - ); - } - - setFetchInterval(newInterval); - return ResponseHandler.ok("Updated interval"); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } -} - -export const createConfHandler = (req: Request, res: Response) => - new ConfHandler(req, res); diff --git a/src/handlers/data.ts b/src/handlers/data.ts deleted file mode 100644 index 5d3bf41..0000000 --- a/src/handlers/data.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { Response, Request } from "express"; -import db from "../config/db"; -import { Table, DataRow } from "../typings/table"; -import { createResponseHandler } from "./response"; -import logger from "../utils/logger"; - -function formatRows(rows: DataRow[]): Record { - return rows.reduce( - ( - acc: Record, - row, - index: number, - ): Record => { - acc[index] = JSON.parse(row.info); - return acc; - }, - {}, - ); -} - -class DatabaseHandler { - private req: Request; - private res: Response; - - constructor(req: Request, res: Response) { - this.req = req; - this.res = res; - } - - latest() { - const ResponseHandler = createResponseHandler(this.res); - db.get( - "SELECT info FROM data ORDER BY timestamp DESC LIMIT 1", - (error: unknown, row: Partial> | undefined) => { - if (error) { - return ResponseHandler.critical(error as string); - } - - if (!row || !row.info) { - return ResponseHandler.error( - "No data available for /data/latest", - 404, - ); - } - - try { - return ResponseHandler.rawData( - JSON.parse(row.info), - "Read latest data", - ); - } catch (error: unknown) { - const errorMsg = - error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - }, - ); - } - - latestRaw(): Promise { - return new Promise((resolve, reject) => { - logger.debug("Reading DB"); - db.get( - "SELECT info FROM data ORDER BY timestamp DESC LIMIT 1", - (error: unknown, row: Partial> | undefined) => { - if (error) { - return reject(`Database query error: ${error}`); - } - - if (!row || !row.info) { - return reject("No data available for /data/latest"); - } - - try { - logger.info("Read latest data"); - const parsedData = JSON.parse(row.info); - logger.debug("Parsed data:", parsedData); - resolve(parsedData); - } catch (error: unknown) { - const errorMsg = - error instanceof Error ? error.message : String(error); - reject(`Error parsing data: ${errorMsg}`); - } - }, - ); - }); - } - - all() { - const ResponseHandler = createResponseHandler(this.res); - const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); - - db.all( - "SELECT info FROM data WHERE timestamp >= ?", - [oneDayAgo], - (error: unknown, rows: Pick[] | undefined) => { - if (error) { - return ResponseHandler.critical(error as string); - } - - if (!rows || rows.length === 0) { - return ResponseHandler.error("No data available", 404); - } - - return ResponseHandler.rawData(formatRows(rows), "Read database"); - }, - ); - } - - clear() { - const ResponseHandler = createResponseHandler(this.res); - db.run("DELETE FROM data", (error: unknown) => { - if (error) { - return ResponseHandler.critical(error as string); - } - - return ResponseHandler.ok("Database cleared successfully"); - }); - } -} - -export const createDatabaseHandler = (req: Request, res: Response) => - new DatabaseHandler(req, res); diff --git a/src/handlers/frontend.ts b/src/handlers/frontend.ts deleted file mode 100644 index 6b2edc5..0000000 --- a/src/handlers/frontend.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { Request, Response } from "express"; -import { createResponseHandler } from "./response"; -import { - hideContainer, - unhideContainer, - addTagToContainer, - removeTagFromContainer, - pinContainer, - unpinContainer, - setLink, - removeLink, - setIcon, - removeIcon, -} from "../controllers/frontendConfiguration"; - -class FrontendHandler { - private req: Request; - private res: Response; - - constructor(req: Request, res: Response) { - this.req = req; - this.res = res; - } - - async show(containerName: string) { - const ResponseHandler = createResponseHandler(this.res); - try { - await unhideContainer(containerName); - return ResponseHandler.ok("Container unhidden successfully."); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - async hide(containerName: string) { - const ResponseHandler = createResponseHandler(this.res); - try { - await hideContainer(containerName); - return ResponseHandler.ok("Hid container succesfully"); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - async addTag(containerName: string, tag: string) { - const ResponseHandler = createResponseHandler(this.res); - try { - await addTagToContainer(containerName, tag); - return ResponseHandler.ok("Tag added successfully."); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - async removeTag(containerName: string, tag: string) { - const ResponseHandler = createResponseHandler(this.res); - try { - await removeTagFromContainer(containerName, tag); - ResponseHandler.ok("Tag removed successfully."); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - async pin(containerName: string) { - const ResponseHandler = createResponseHandler(this.res); - try { - await pinContainer(containerName); - return ResponseHandler.ok("Container pinned successfully."); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - async unPin(containerName: string) { - const ResponseHandler = createResponseHandler(this.res); - try { - await unpinContainer(containerName); - return ResponseHandler.ok("Container unpinned successfully."); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - async addLink(containerName: string, link: string) { - const ResponseHandler = createResponseHandler(this.res); - try { - await setLink(containerName, link); - return ResponseHandler.ok("Link added successfully."); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - async removeLink(containerName: string) { - const ResponseHandler = createResponseHandler(this.res); - try { - await removeLink(containerName); - return ResponseHandler.ok("Removed link succesfully"); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - async addIcon(containerName: string, icon: string, useCustomIcon: string) { - const ResponseHandler = createResponseHandler(this.res); - const iconBool = useCustomIcon === "true"; - try { - await setIcon(containerName, icon, iconBool); - return ResponseHandler.ok("Icon added successfully."); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - async removeIcon(containerName: string) { - const ResponseHandler = createResponseHandler(this.res); - try { - await removeIcon(containerName); - return ResponseHandler.ok("Icon removed successfully."); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } -} - -export const createFrontendHandler = (req: Request, res: Response) => - new FrontendHandler(req, res); diff --git a/src/handlers/graph.ts b/src/handlers/graph.ts deleted file mode 100644 index 12e0572..0000000 --- a/src/handlers/graph.ts +++ /dev/null @@ -1,82 +0,0 @@ -import cytoscape from "cytoscape"; -import logger from "../utils/logger"; -import { AllContainerData, ContainerData } from "./../typings/dockerConfig"; -import { atomicWrite } from "../utils/atomicWrite"; - -const CACHE_DIR_JSON = "./src/data/graph.json"; - -async function generateGraphJSON( - allContainerData: AllContainerData, -): Promise { - try { - logger.info("generateGraphJSON >>> Starting generation"); - - const graphData = { - nodes: [] as cytoscape.ElementDefinition[], - edges: [] as cytoscape.ElementDefinition[], - }; - - for (const [hostName, containers] of Object.entries(allContainerData)) { - if ("error" in containers) { - graphData.nodes.push({ - data: { - id: hostName, - label: `Host: ${hostName} Error: ${containers.error}`, - type: "server", - error: true, - }, - }); - } else { - const containerList = containers as ContainerData[]; - - // Host node with container count and metadata - graphData.nodes.push({ - data: { - id: hostName, - label: `${hostName}\n${containerList.length} Containers`, - type: "server", - hostName, - containerCount: containerList.length, - }, - }); - - for (const container of containerList) { - const { id, ...otherContainerProps } = container; - - graphData.nodes.push({ - data: { - id: id, - label: `${container.name}\n${container.state.toUpperCase()}`, - type: "container", - ...otherContainerProps, - }, - }); - - // Edge between host and container - graphData.edges.push({ - data: { - id: `${hostName}-${container.id}`, - source: hostName, - target: container.id, - connectionType: "host-container", - }, - }); - } - } - } - - atomicWrite(CACHE_DIR_JSON, JSON.stringify(graphData, null, 2)); - logger.info("generateGraphJSON <<< JSON file generated successfully"); - return true; - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - return false; - } -} - -function getGraphFilePath() { - return { json: CACHE_DIR_JSON }; -} - -export { generateGraphJSON, getGraphFilePath }; diff --git a/src/handlers/ha.ts b/src/handlers/ha.ts deleted file mode 100644 index 16c9ae1..0000000 --- a/src/handlers/ha.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Request, Response } from "express"; -import logger from "../utils/logger"; -import { - readConfig, - prepareFilesForSync, - ensureFileExists, -} from "../controllers/highAvailability"; -import { createResponseHandler } from "./response"; - -class HaHandler { - private req: Request; - private res: Response; - - constructor(req: Request, res: Response) { - this.req = req; - this.res = res; - } - - async config() { - const ResponseHandler = createResponseHandler(this.res); - try { - const data = await readConfig(); - return ResponseHandler.rawData(data, "Fetched HA-Config"); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - async sync(req: Request) { - const ResponseHandler = createResponseHandler(this.res); - try { - const { files } = req.body; - logger.info("Received synchronization request from master node."); - if (!files || typeof files !== "object") { - return ResponseHandler.error( - "Invalid request: 'files' object is missing or invalid.", - 400, - ); - } - - for (const [filePath, content] of Object.entries(files)) { - await ensureFileExists(filePath, content as string); - } - - return ResponseHandler.ok("Synchronization completed successfully."); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - async prepare() { - const ResponseHandler = createResponseHandler(this.res); - try { - logger.info("Preparing files for synchronization."); - const fileData = await prepareFilesForSync(); - return ResponseHandler.rawData( - fileData, - "Done preparing files for synchronization", - ); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } -} - -export const createHaHandler = (req: Request, res: Response) => - new HaHandler(req, res); diff --git a/src/handlers/notification.ts b/src/handlers/notification.ts deleted file mode 100644 index 9c10a59..0000000 --- a/src/handlers/notification.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { Request, Response } from "express"; -import fs from "fs"; -import notify from "../utils/notifications/_notify"; -const dataTemplate = "./src/data/template.json"; -import { TemplateData } from "../typings/template"; -import { createResponseHandler } from "./response"; - -function isTemplateData(data: TemplateData): data is TemplateData { - return ( - data !== null && typeof data === "object" && typeof data.text === "string" - ); -} - -class NotificationHandler { - private req: Request; - private res: Response; - - constructor(req: Request, res: Response) { - this.req = req; - this.res = res; - } - - getTemplate() { - const ResponseHandler = createResponseHandler(this.res); - try { - fs.readFile(dataTemplate, "utf-8", (error: unknown, data) => { - if (error) { - return ResponseHandler.error(error as string, 400); - } - return ResponseHandler.rawData( - JSON.parse(data), - "Fetched notification template", - ); - }); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - setTemplate(req: Request) { - const ResponseHandler = createResponseHandler(this.res); - const newTemplate: TemplateData = req.body; - - try { - if (!isTemplateData(newTemplate)) { - return ResponseHandler.error( - "Invalid input format. Expected JSON with a 'text' field.", - 400, - ); - } - - fs.writeFileSync(dataTemplate, JSON.stringify(newTemplate, null, 2)); - return ResponseHandler.ok("Template updated successfully."); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - async test(req: Request) { - const { type, containerId } = req.params; - const ResponseHandler = createResponseHandler(this.res); - - try { - await notify(type, containerId); - return ResponseHandler.ok("Sent test notification"); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } -} - -export const createNotificationHandler = (req: Request, res: Response) => - new NotificationHandler(req, res); diff --git a/src/handlers/response.ts b/src/handlers/response.ts deleted file mode 100644 index ee06210..0000000 --- a/src/handlers/response.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Response } from "express"; -import logger from "../utils/logger"; - -class ResponseHandler { - private res: Response; - - constructor(res: Response) { - this.res = res; - } - - rawData(data: unknown, message: string) { - logger.info(message); - this.res.status(200).json(data); - } - - ok(message: string) { - logger.info(message); - this.res.status(200).json({ status: "success", message }); - } - - denied(message: string) { - logger.warn(message); - this.res.status(403).json({ status: "denied", message }); - } - - error(message: string, code: number) { - logger.error(`Code: ${code} - ${message}`); - this.res.status(code).json({ status: "error", message }); - } - - critical(log: string) { - logger.error(log.replace(/\n|\r/g, "")); - this.res.status(500).json({ - status: "critical", - message: "Please see the server logs for more info", - }); - } -} - -export const createResponseHandler = (res: Response) => - new ResponseHandler(res); diff --git a/src/handlers/stack.ts b/src/handlers/stack.ts deleted file mode 100644 index 0f15e16..0000000 --- a/src/handlers/stack.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { Response, Request } from "express"; -import { - createStack, - getStackConfig, - getStackCompose, - writeEnvFile, - getEnvFile, -} from "../config/stacks"; -import { DockerComposeFile } from "../typings/dockerCompose"; -import logger from "../utils/logger"; -import * as compose from "docker-compose"; -import { createResponseHandler } from "./response"; -import { stackConfig } from "../typings/stackConfig"; -import { dockerStackEnv } from "../typings/dockerStackEnv"; -import path from "path"; - -const PROJECT_ROOT = path.resolve(__dirname, "../.."); - -export async function validate(name: string): Promise { - const config: stackConfig = JSON.parse(await getStackConfig()); - if (!config.stacks.find((element) => element === name)) { - throw new Error("Stack not found"); - } - - return true; -} - -async function composeAction(option: string, name: string): Promise { - const composeFile: string = path.join(PROJECT_ROOT, `stacks/${name}`); - try { - switch (option) { - case "start": { - await compose.upAll({ cwd: composeFile, log: false }); - break; - } - case "stop": { - await compose.downAll({ cwd: composeFile, log: false }); - break; - } - default: - throw new Error(`Invalid option: ${option}`); - } - } catch (err) { - let errorMessage: string; - const portAllocated: string = "port is already allocated"; - - if (err instanceof Error) { - errorMessage = err.message; - } else if (typeof err === "object" && err !== null) { - errorMessage = JSON.stringify(err); - } else { - errorMessage = String(err); - } - - if (errorMessage.search(portAllocated)) { - logger.error("Port(s) already allocated"); - } - throw new Error(errorMessage); - } -} - -class StackHandler { - private req: Request; - private res: Response; - - constructor(req: Request, res: Response) { - this.req = req; - this.res = res; - } - - async createStack(req: Request, res: Response) { - const ResponseHandler = createResponseHandler(res); - try { - const name: string = req.params.name; - const content: DockerComposeFile = req.body; - let override = false; - override = req.query.override == "true"; - - await createStack(name, content, override); - return ResponseHandler.ok("Stack created"); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - async start(req: Request, res: Response) { - const ResponseHandler = createResponseHandler(res); - try { - const name: string = req.params.name; - await validate(name); - await composeAction("start", name); - return ResponseHandler.ok("Stack started"); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - async stop(req: Request, res: Response) { - const ResponseHandler = createResponseHandler(res); - try { - const name: string = req.params.name; - await validate(name); - await composeAction("stop", name); - return ResponseHandler.ok("Stack stopped"); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - async stackCompose(req: Request, res: Response) { - const ResponseHandler = createResponseHandler(res); - try { - const { name } = req.params; - return ResponseHandler.rawData( - await getStackCompose(name), - "Stack compose fetched", - ); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg.replace(/\n|\r/g, "")); - throw new Error(errorMsg); - } - } - - async setStackEnv(req: Request, res: Response) { - const ResponseHandler = createResponseHandler(res); - try { - const data: dockerStackEnv = req.body; - const name: string = req.params.name; - if (await writeEnvFile(name, data)) { - return ResponseHandler.ok("Wrote docker.env"); - } else { - return ResponseHandler.critical( - "Something went wrong while writing the env File!", - ); - } - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg.replace(/\n|\r/g, "")); - throw new Error(errorMsg); - } - } - - async getStackEnv(req: Request, res: Response) { - const ResponseHandler = createResponseHandler(res); - try { - const name: string = req.params.name; - const data = await getEnvFile(name); - if (data == null) { - return ResponseHandler.error( - "No environment file found for this Stack!", - 404, - ); - } - return ResponseHandler.rawData(data, "Read docker.env"); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg.replace(/\n|\r/g, "")); - throw new Error(errorMsg); - } - } -} - -export const createStackHandler = (req: Request, res: Response) => - new StackHandler(req, res); diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..dcca5a9 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,45 @@ +import { Elysia } from "elysia"; +import { swagger } from "@elysiajs/swagger"; +import { loadPlugins } from "~/core/plugins/loader"; +import { dockerRoutes } from "~/routes/docker"; +import { logRoutes } from "~/routes/container-logs"; +import { backendLogs } from "./routes/logs"; +import { dbFunctions } from "~/core/database/repository"; +import { logger } from "~/core/utils/logger"; + +dbFunctions.init(); + +const app = new Elysia() + .use( + swagger({ + documentation: { + info: { + title: "DockStatAPI", + version: "0.1.0", + description: "Docker monitoring API with plugin support", + }, + }, + }), + ) + .use(dockerRoutes) + .use(logRoutes) + .use(backendLogs) + .get("/health", () => ({ status: "healthy" })); + +async function startServer() { + try { + await loadPlugins("./plugins"); + + app.listen(3000, ({ hostname, port }) => { + logger.info(`🦊 Elysia is running at http://${hostname}:${port}`); + logger.info( + `📚 API Documentation available at http://${hostname}:${port}/swagger`, + ); + }); + } catch (error) { + logger.error("Failed to start server:", error); + process.exit(1); + } +} + +startServer(); diff --git a/src/init.ts b/src/init.ts deleted file mode 100644 index 188542f..0000000 --- a/src/init.ts +++ /dev/null @@ -1,69 +0,0 @@ -import express, { Request, Response, NextFunction } from "express"; -import process from "node:process"; -import swaggerDocs from "./utils/swaggerDocs"; -import auth from "./routes/auth/routes"; -import data from "./routes/data/routes"; -import frontend from "./routes/frontendController/routes"; -import api from "./routes/getter/routes"; -import notificationService from "./routes/notifications/routes"; -import conf from "./routes/setter/routes"; -import graph from "./routes/graphs/routes"; -import authMiddleware from "./middleware/authMiddleware"; -import ha from "./routes/highavailability/routes"; -import trustedProxies from "./controllers/proxy"; -import { limiter } from "./middleware/rateLimiter"; -import { scheduleFetch } from "./controllers/scheduler"; -import { Server } from 'http'; -import cors from "cors"; -import { setupWebSocket } from "./utils/webSocket"; -import stacks from "./routes/stack/routes"; -import { blockWhileLocked } from "./middleware/checkLock"; -import logger from "./utils/logger"; -import initFiles from "./config/initFiles"; - -const LAB = [limiter, authMiddleware, blockWhileLocked]; - -const initializeApp = (app: express.Application, server: Server): void => { - initFiles(); - - try { - logger.debug("Starting Websocket server, with these endpoints:"); - logger.debug("ws://localhost:9876/wss/container-data") - logger.debug("ws://localhost:9876/wss/server-logs") - setupWebSocket(server); - } catch (error: unknown) { - logger.error("Error starting WebSocket: ", error) - } - - app.use(cors()); - app.use(express.json()); - - if (process.env.NODE_ENV !== "production") { - app.use("/api-docs", (req: Request, res: Response, next: NextFunction) => - next(), - ); - app.get("/", (req: Request, res: Response) => { - res.redirect("/api-docs"); - }); - swaggerDocs(app); - } - - trustedProxies(app); - scheduleFetch(); - - app.use("/api", LAB, api); - app.use("/conf", LAB, conf); - app.use("/auth", LAB, auth); - app.use("/data", LAB, data); - app.use("/frontend", LAB, frontend); - app.use("/graph", LAB, graph); - app.use("/notification-service", LAB, notificationService); - app.use("/stacks", LAB, stacks); - app.use("/ha", limiter, authMiddleware, ha); - - process.on("exit", (code: number) => { - logger.warn(`Server exiting (Code: ${code})`); - }); -}; - -export default initializeApp; diff --git a/src/middleware/authMiddleware.ts b/src/middleware/authMiddleware.ts deleted file mode 100644 index 414b276..0000000 --- a/src/middleware/authMiddleware.ts +++ /dev/null @@ -1,51 +0,0 @@ -import bcrypt from "bcrypt"; -import { Request, Response, NextFunction } from "express"; -import logger from "../utils/logger"; -import { rateLimitedReadFile } from "../utils/rateLimitFS"; -import { createResponseHandler } from "../handlers/response"; -const passwordFile = "./src/data/password.json"; -const passwordBool = "./src/data/usePassword.txt"; - -async function authMiddleware( - req: Request, - res: Response, - next: NextFunction, -): Promise { - const ResponseHandler = createResponseHandler(res); - try { - const authStatusData = await rateLimitedReadFile(passwordBool); - const isAuthEnabled = authStatusData.trim() === "true"; - - if (!isAuthEnabled) { - logger.warn("You are not using authentication, please enable it."); - logger.debug("Authentication disabled, skipping login process..."); - return next(); - } - - const providedPassword = req.headers["x-password"]; - if (!providedPassword) { - ResponseHandler.denied("Password required"); - return; - } - - const passwordData = await rateLimitedReadFile(passwordFile); - const storedData = JSON.parse(passwordData); - - const passwordMatch = await bcrypt.compare( - providedPassword as string, - storedData.hash, - ); - if (!passwordMatch) { - ResponseHandler.error("Invalid Password", 402); - return; - } - - logger.debug("Authentication succesfull"); - next(); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } -} - -export default authMiddleware; diff --git a/src/middleware/checkLock.ts b/src/middleware/checkLock.ts deleted file mode 100644 index c01540f..0000000 --- a/src/middleware/checkLock.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { rateLimitedExistsSync } from "../utils/rateLimitFS"; -import { createResponseHandler } from "../handlers/response"; - -const lockFilePath = "./src/data/ha.lock"; - -export async function blockWhileLocked( - req: Request, - res: Response, - next: NextFunction, -): Promise { - const ResponseHandler = createResponseHandler(res); - if (await rateLimitedExistsSync(lockFilePath)) { - ResponseHandler.error( - "Service unavailable. The high-availability lock is currently active. Please try again later.", - 503, - ); - } else { - next(); - } -} diff --git a/src/middleware/rateLimiter.ts b/src/middleware/rateLimiter.ts deleted file mode 100644 index dc64af2..0000000 --- a/src/middleware/rateLimiter.ts +++ /dev/null @@ -1,8 +0,0 @@ -import rateLimit from "express-rate-limit"; - -export const limiter = rateLimit({ - windowMs: 5 * 60 * 1000, // 5 minutes - limit: 300, // Limit each IP to 300 requests per `window` (here, per 5 minutes) - standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers - legacyHeaders: false, // Disable the `X-RateLimit-*` headers -}); diff --git a/src/misc/.tmux.sh b/src/misc/.tmux.sh deleted file mode 100644 index a929a1a..0000000 --- a/src/misc/.tmux.sh +++ /dev/null @@ -1 +0,0 @@ -[ -z "$TMUX" ] && tmux new-session -d -s docker 'docker compose -f docker/docker-compose.yaml logs -f master' \; rename-window 'master' \; new-window 'docker compose -f docker/docker-compose.yaml logs -f slave' \; rename-window 'slave' \; new-window 'docker compose -f docker/docker-compose.yaml logs -f test-socket-proxy' \; rename-window 'proxy' \; attach-session || echo 'Already inside a tmux session. Exiting.' diff --git a/src/misc/createEnvDev.sh b/src/misc/createEnvDev.sh deleted file mode 100755 index 1f231aa..0000000 --- a/src/misc/createEnvDev.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/bin/bash - -# Version -VERSION="$(cat ./package.json | grep version | cut -d '"' -f 4)" - -# Automatic Stack environment management -AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT="${AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT:-true}" - -# Docker -if grep -q '/docker' /proc/1/cgroup 2>/dev/null || [ -f /.dockerenv ]; then - RUNNING_IN_DOCKER="true" -else - RUNNING_IN_DOCKER="false" -fi - -# Default dev log level -LOG_LEVEL="${LOG_LEVEL:-debug}" - -echo -n "\ -{ - \"VERSION\": \"${VERSION}\", - \"RUNNING_IN_DOCKER\": \"${RUNNING_IN_DOCKER}\", - \"TRUSTED_PROXIES\": \"${TRUSTED_PROXIES}\", - \"HA_MASTER\": \"${HA_MASTER}\", - \"HA_MASTER_IP\": \"${HA_MASTER_IP}\", - \"HA_NODE\": \"${HA_NODE}\", - \"HA_UNSAFE\": \"${HA_UNSAFE}\", - \"DISCORD_WEBHOOK_URL\": \"${DISCORD_WEBHOOK_URL}\", - \"EMAIL_SENDER\": \"${EMAIL_SENDER}\", - \"EMAIL_RECIPIENT\": \"${EMAIL_RECIPIENT}\", - \"EMAIL_PASSWORD\": \"${EMAIL_PASSWORD}\", - \"EMAIL_SERVICE\": \"${EMAIL_SERVICE}\", - \"PUSHBULLET_ACCESS_TOKEN\": \"${PUSHBULLET_ACCESS_TOKEN}\", - \"PUSHOVER_USER_KEY\": \"${PUSHOVER_USER_KEY}\", - \"PUSHOVER_API_TOKEN\": \"${PUSHOVER_API_TOKEN}\", - \"SLACK_WEBHOOK_URL\": \"${SLACK_WEBHOOK_URL}\", - \"TELEGRAM_BOT_TOKEN\": \"${TELEGRAM_BOT_TOKEN}\", - \"TELEGRAM_CHAT_ID\": \"${TELEGRAM_CHAT_ID}\", - \"WHATSAPP_API_URL\": \"${WHATSAPP_API_URL}\", - \"WHATSAPP_RECIPIENT\": \"${WHATSAPP_RECIPIENT}\", - \"AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT\": \"${AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT}\", - \"LOG_LEVEL\": \"${LOG_LEVEL}\" -} \ -" > ./src/data/variables.json || exit 1 diff --git a/src/misc/createEnvFile.sh b/src/misc/createEnvFile.sh deleted file mode 100755 index 0fbd15d..0000000 --- a/src/misc/createEnvFile.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/bin/bash - -# Version -VERSION="$(cat ./package.json | grep version | cut -d '"' -f 4)" - -# Automatic Stack environment management -AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT="${AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT:-true}" - -# Docker -if grep -q '/docker' /proc/1/cgroup 2>/dev/null || [ -f /.dockerenv ]; then - RUNNING_IN_DOCKER="true" -else - RUNNING_IN_DOCKER="false" -fi - -# Default log level -LOG_LEVEL="${LOG_LEVEL:-info}" - -echo -n "\ -{ - \"VERSION\": \"${VERSION}\", - \"RUNNING_IN_DOCKER\": \"${RUNNING_IN_DOCKER}\", - \"TRUSTED_PROXIES\": \"${TRUSTED_PROXIES}\", - \"HA_MASTER\": \"${HA_MASTER}\", - \"HA_MASTER_IP\": \"${HA_MASTER_IP}\", - \"HA_NODE\": \"${HA_NODE}\", - \"HA_UNSAFE\": \"${HA_UNSAFE}\", - \"DISCORD_WEBHOOK_URL\": \"${DISCORD_WEBHOOK_URL}\", - \"EMAIL_SENDER\": \"${EMAIL_SENDER}\", - \"EMAIL_RECIPIENT\": \"${EMAIL_RECIPIENT}\", - \"EMAIL_PASSWORD\": \"${EMAIL_PASSWORD}\", - \"EMAIL_SERVICE\": \"${EMAIL_SERVICE}\", - \"PUSHBULLET_ACCESS_TOKEN\": \"${PUSHBULLET_ACCESS_TOKEN}\", - \"PUSHOVER_USER_KEY\": \"${PUSHOVER_USER_KEY}\", - \"PUSHOVER_API_TOKEN\": \"${PUSHOVER_API_TOKEN}\", - \"SLACK_WEBHOOK_URL\": \"${SLACK_WEBHOOK_URL}\", - \"TELEGRAM_BOT_TOKEN\": \"${TELEGRAM_BOT_TOKEN}\", - \"TELEGRAM_CHAT_ID\": \"${TELEGRAM_CHAT_ID}\", - \"WHATSAPP_API_URL\": \"${WHATSAPP_API_URL}\", - \"WHATSAPP_RECIPIENT\": \"${WHATSAPP_RECIPIENT}\", - \"AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT\": \"${AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT}\", - \"LOG_LEVEL\": \"${LOG_LEVEL}\" -} \ -" > /api/src/data/variables.json || exit 1 diff --git a/src/misc/credits.sh b/src/misc/credits.sh deleted file mode 100755 index 3db14f6..0000000 --- a/src/misc/credits.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/bash - -if ! command -v jq 2>&1 >/dev/null -then - echo "ERROR: jq could not be found" - exit 1 -fi - - -LICENSE_JSON=$(npx license-checker \ - --exclude 'MIT, MIT-0, MIT OR X11, BSD, ISC, Unlicense, CC0-1.0, Python-2.0: 1' \ - --json) - -{ - echo -e "# CREDITS\n" - echo -e "This file shows all npm packages used in DockStatAPI (also Dev packages)\n" -} > CREDITS.md - -jq -r ' - to_entries | - group_by(.value.licenses)[] | - "### License: \(.[0].value.licenses)\n\n" + - "| Name | Repository | Publisher |\n|------|-------------|-----------|\n" + - (map( - "| \(.key) | \(.value.repository // "N/A") | \(.value.publisher // "N/A") |" - ) | join("\n")) + "\n\n" -' <<< "$LICENSE_JSON" >> CREDITS.md - -echo "Markdown file with license information has been created: CREDITS.md" diff --git a/src/misc/dependencyGraphs/.dependency-cruiser.cjs b/src/misc/dependencyGraphs/.dependency-cruiser.cjs deleted file mode 100644 index d734a68..0000000 --- a/src/misc/dependencyGraphs/.dependency-cruiser.cjs +++ /dev/null @@ -1,359 +0,0 @@ -/** @type {import('dependency-cruiser').IConfiguration} */ -module.exports = { - forbidden: [ - { - name: "no-circular", - severity: "warn", - comment: - "This dependency is part of a circular relationship. You might want to revise " + - "your solution (i.e. use dependency inversion, make sure the modules have a single responsibility) ", - from: {}, - to: { - circular: true, - }, - }, - { - name: "no-orphans", - comment: - "This is an orphan module - it's likely not used (anymore?). Either use it or " + - "remove it. If it's logical this module is an orphan (i.e. it's a config file), " + - "add an exception for it in your dependency-cruiser configuration. By default " + - "this rule does not scrutinize dot-files (e.g. .eslintrc.js), TypeScript declaration " + - "files (.d.ts), tsconfig.json and some of the babel and webpack configs.", - severity: "warn", - from: { - orphan: true, - pathNot: [ - "(^|/)[.][^/]+[.](?:js|cjs|mjs|ts|cts|mts|json)$", // dot files - "[.]d[.]ts$", // TypeScript declaration files - "(^|/)tsconfig[.]json$", // TypeScript config - "(^|/)(?:babel|webpack)[.]config[.](?:js|cjs|mjs|ts|cts|mts|json)$", // other configs - ], - }, - to: {}, - }, - { - name: "no-deprecated-core", - comment: - "A module depends on a node core module that has been deprecated. Find an alternative - these are " + - "bound to exist - node doesn't deprecate lightly.", - severity: "warn", - from: {}, - to: { - dependencyTypes: ["core"], - path: [ - "^v8/tools/codemap$", - "^v8/tools/consarray$", - "^v8/tools/csvparser$", - "^v8/tools/logreader$", - "^v8/tools/profile_view$", - "^v8/tools/profile$", - "^v8/tools/SourceMap$", - "^v8/tools/splaytree$", - "^v8/tools/tickprocessor-driver$", - "^v8/tools/tickprocessor$", - "^node-inspect/lib/_inspect$", - "^node-inspect/lib/internal/inspect_client$", - "^node-inspect/lib/internal/inspect_repl$", - "^async_hooks$", - "^punycode$", - "^domain$", - "^constants$", - "^sys$", - "^_linklist$", - "^_stream_wrap$", - ], - }, - }, - { - name: "not-to-deprecated", - comment: - "This module uses a (version of an) npm module that has been deprecated. Either upgrade to a later " + - "version of that module, or find an alternative. Deprecated modules are a security risk.", - severity: "warn", - from: {}, - to: { - dependencyTypes: ["deprecated"], - }, - }, - { - name: "no-non-package-json", - severity: "error", - comment: - "This module depends on an npm package that isn't in the 'dependencies' section of your package.json. " + - "That's problematic as the package either (1) won't be available on live (2 - worse) will be " + - "available on live with an non-guaranteed version. Fix it by adding the package to the dependencies " + - "in your package.json.", - from: {}, - to: { - dependencyTypes: ["npm-no-pkg", "npm-unknown"], - }, - }, - { - name: "not-to-unresolvable", - comment: - "This module depends on a module that cannot be found ('resolved to disk'). If it's an npm " + - "module: add it to your package.json. In all other cases you likely already know what to do.", - severity: "error", - from: {}, - to: { - couldNotResolve: true, - }, - }, - { - name: "no-duplicate-dep-types", - comment: - "Likely this module depends on an external ('npm') package that occurs more than once " + - "in your package.json i.e. bot as a devDependencies and in dependencies. This will cause " + - "maintenance problems later on.", - severity: "warn", - from: {}, - to: { - moreThanOneDependencyType: true, - // as it's pretty common to have a type import be a type only import - // _and_ (e.g.) a devDependency - don't consider type-only dependency - // types for this rule - dependencyTypesNot: ["type-only"], - }, - }, - - /* rules you might want to tweak for your specific situation: */ - - { - name: "not-to-spec", - comment: - "This module depends on a spec (test) file. The sole responsibility of a spec file is to test code. " + - "If there's something in a spec that's of use to other modules, it doesn't have that single " + - "responsibility anymore. Factor it out into (e.g.) a separate utility/ helper or a mock.", - severity: "error", - from: {}, - to: { - path: "[.](?:spec|test)[.](?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$", - }, - }, - { - name: "not-to-dev-dep", - severity: "error", - comment: - "This module depends on an npm package from the 'devDependencies' section of your " + - "package.json. It looks like something that ships to production, though. To prevent problems " + - "with npm packages that aren't there on production declare it (only!) in the 'dependencies'" + - "section of your package.json. If this module is development only - add it to the " + - "from.pathNot re of the not-to-dev-dep rule in the dependency-cruiser configuration", - from: { - path: "^(./)", - pathNot: "[.](?:spec|test)[.](?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$", - }, - to: { - dependencyTypes: ["npm-dev"], - // type only dependencies are not a problem as they don't end up in the - // production code or are ignored by the runtime. - dependencyTypesNot: ["type-only"], - pathNot: ["node_modules/@types/"], - }, - }, - { - name: "optional-deps-used", - severity: "info", - comment: - "This module depends on an npm package that is declared as an optional dependency " + - "in your package.json. As this makes sense in limited situations only, it's flagged here. " + - "If you're using an optional dependency here by design - add an exception to your" + - "dependency-cruiser configuration.", - from: {}, - to: { - dependencyTypes: ["npm-optional"], - }, - }, - { - name: "peer-deps-used", - comment: - "This module depends on an npm package that is declared as a peer dependency " + - "in your package.json. This makes sense if your package is e.g. a plugin, but in " + - "other cases - maybe not so much. If the use of a peer dependency is intentional " + - "add an exception to your dependency-cruiser configuration.", - severity: "warn", - from: {}, - to: { - dependencyTypes: ["npm-peer"], - }, - }, - ], - options: { - /* Which modules not to follow further when encountered */ - doNotFollow: { - /* path: an array of regular expressions in strings to match against */ - path: ["../node_modules"], - }, - - /* Which modules to exclude */ - // exclude : { - // /* path: an array of regular expressions in strings to match against */ - // path: '', - // }, - - /* Which modules to exclusively include (array of regular expressions in strings) - dependency-cruiser will skip everything not matching this pattern - */ - // includeOnly : [''], - - /* List of module systems to cruise. - When left out dependency-cruiser will fall back to the list of _all_ - module systems it knows of. It's the default because it's the safe option - It might come at a performance penalty, though. - moduleSystems: ['amd', 'cjs', 'es6', 'tsd'] - - As in practice only commonjs ('cjs') and ecmascript modules ('es6') - are widely used, you can limit the moduleSystems to those. - */ - - // moduleSystems: ['cjs', 'es6'], - - /* prefix for links in html and svg output (e.g. 'https://github.com/you/yourrepo/blob/main/' - to open it on your online repo or `vscode://file/${process.cwd()}/` to - open it in visual studio code), - */ - // prefix: `vscode://file/${process.cwd()}/`, - - /* false (the default): ignore dependencies that only exist before typescript-to-javascript compilation - true: also detect dependencies that only exist before typescript-to-javascript compilation - "specify": for each dependency identify whether it only exists before compilation or also after - */ - // tsPreCompilationDeps: false, - - /* list of extensions to scan that aren't javascript or compile-to-javascript. - Empty by default. Only put extensions in here that you want to take into - account that are _not_ parsable. - */ - // extraExtensionsToScan: [".json", ".jpg", ".png", ".svg", ".webp"], - - /* if true combines the package.jsons found from the module up to the base - folder the cruise is initiated from. Useful for how (some) mono-repos - manage dependencies & dependency definitions. - */ - // combinedDependencies: false, - - /* if true leave symlinks untouched, otherwise use the realpath */ - // preserveSymlinks: false, - - /* TypeScript project file ('tsconfig.json') to use for - (1) compilation and - (2) resolution (e.g. with the paths property) - - The (optional) fileName attribute specifies which file to take (relative to - dependency-cruiser's current working directory). When not provided - defaults to './tsconfig.json'. - */ - //tsConfig: { - //fileName: "../tsconfig.json", - //}, - - /* Webpack configuration to use to get resolve options from. - - The (optional) fileName attribute specifies which file to take (relative - to dependency-cruiser's current working directory. When not provided defaults - to './webpack.conf.js'. - - The (optional) `env` and `arguments` attributes contain the parameters - to be passed if your webpack config is a function and takes them (see - webpack documentation for details) - */ - // webpackConfig: { - // fileName: 'webpack.config.js', - // env: {}, - // arguments: {} - // }, - - /* Babel config ('.babelrc', '.babelrc.json', '.babelrc.json5', ...) to use - for compilation - */ - // babelConfig: { - // fileName: '.babelrc', - // }, - - /* List of strings you have in use in addition to cjs/ es6 requires - & imports to declare module dependencies. Use this e.g. if you've - re-declared require, use a require-wrapper or use window.require as - a hack. - */ - // exoticRequireStrings: [], - - /* options to pass on to enhanced-resolve, the package dependency-cruiser - uses to resolve module references to disk. The values below should be - suitable for most situations - - If you use webpack: you can also set these in webpack.conf.js. The set - there will override the ones specified here. - */ - enhancedResolveOptions: { - /* What to consider as an 'exports' field in package.jsons */ - exportsFields: ["exports"], - /* List of conditions to check for in the exports field. - Only works when the 'exportsFields' array is non-empty. - */ - conditionNames: ["import", "require", "node", "default", "types"], - /* - The extensions, by default are the same as the ones dependency-cruiser - can access (run `npx depcruise --info` to see which ones that are in - _your_ environment). If that list is larger than you need you can pass - the extensions you actually use (e.g. ["", ".jsx"]). This can speed - up module resolution, which is the most expensive step. - */ - extensions: ["", ".jsx", ".ts", ".tsx"], - /* What to consider a 'main' field in package.json */ - mainFields: ["module", "main", "types", "typings"], - /* - A list of alias fields in package.jsons - See [this specification](https://github.com/defunctzombie/package-browser-field-spec) and - the webpack [resolve.alias](https://webpack.js.org/configuration/resolve/#resolvealiasfields) - documentation - - Defaults to an empty array (= don't use alias fields). - */ - // aliasFields: ["browser"], - }, - reporterOptions: { - dot: { - /* pattern of modules that can be consolidated in the detailed - graphical dependency graph. The default pattern in this configuration - collapses everything in node_modules to one folder deep so you see - the external modules, but their innards. - */ - collapsePattern: "node_modules/(?:@[^/]+/[^/]+|[^/]+)", - - /* Options to tweak the appearance of your graph.See - https://github.com/sverweij/dependency-cruiser/blob/main/doc/options-reference.md#reporteroptions - for details and some examples. If you don't specify a theme - dependency-cruiser falls back to a built-in one. - */ - theme: { - graph: { - /* splines: "ortho" gives straight lines, but is slow on big graphs - splines: "true" gives bezier curves (fast, not as nice as ortho) - */ - ortho: "true", - }, - }, - }, - archi: { - /* pattern of modules that can be consolidated in the high level - graphical dependency graph. If you use the high level graphical - dependency graph reporter (`archi`) you probably want to tweak - this collapsePattern to your situation. - */ - collapsePattern: - "^(?:packages|src|lib(s?)|app(s?)|bin|test(s?)|spec(s?))/[^/]+|node_modules/(?:@[^/]+/[^/]+|[^/]+)", - - /* Options to tweak the appearance of your graph. If you don't specify a - theme for 'archi' dependency-cruiser will use the one specified in the - dot section above and otherwise use the default one. - */ - // theme: { }, - }, - text: { - highlightFocused: true, - }, - }, - }, -}; -// generated: dependency-cruiser@16.5.0 on 2024-11-08T20:57:37.261Z diff --git a/src/misc/dependencyGraphs/createDependencyGraph.sh b/src/misc/dependencyGraphs/createDependencyGraph.sh deleted file mode 100755 index 5fe007a..0000000 --- a/src/misc/dependencyGraphs/createDependencyGraph.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/bin/bash -TMP=$(mktemp) -IGNORE="node_modules|logger|.dependency-cruiser|path|fs|os|https|net|process|util" - -cat ./src/init.ts | grep "./routes" | awk '{print $2,$4}' > $TMP - -spawn_worker(){ - local line="$1" - local target_route="$(echo "$line" | cut -d '"' -f2 | sed 's|^./routes|./src/routes|').ts" - local route=$(echo "$line" | awk '{print $1}') - - echo -e "\nRoute: $route \n${target_route}" - - test=true depcruise \ - -c ./src/misc/dependencyGraphs/.dependency-cruiser.cjs \ - -p cli-feedback \ - -T mermaid \ - -x "$IGNORE" \ - -f ./src/misc/dependencyGraphs/mermaid-${route}.txt \ - ${target_route} || exit 1 -} - -while read line; do - spawn_worker "$line" & -done < <(cat $TMP) - -npx depcruise \ - -c ./src/misc/dependencyGraphs/.dependency-cruiser.cjs \ - -p cli-feedback \ - -T mermaid \ - -x "$IGNORE" \ - -f ./src/misc/dependencyGraphs/mermaid-all.txt \ - ./src/server.ts || exit 1 - -wait - -find ./src/misc/dependencyGraphs -type f -name "*.txt" -exec sed -i 's/flowchart LR/flowchart TB/g' {} + - -echo -e "\n========\n\n DONE\n\n========" - -exit 0 diff --git a/src/misc/dependencyGraphs/mermaid-all.txt b/src/misc/dependencyGraphs/mermaid-all.txt deleted file mode 100644 index 1cb2ebe..0000000 --- a/src/misc/dependencyGraphs/mermaid-all.txt +++ /dev/null @@ -1,113 +0,0 @@ -flowchart TB - -subgraph 0["src"] -1["server.ts"] -2["init.ts"] -subgraph 3["config"] -4["initFiles.ts"] -7["variables.ts"] -B["db.ts"] -end -subgraph 5["controllers"] -6["proxy.ts"] -A["scheduler.ts"] -C["fetchData.ts"] -N["auth.ts"] -U["frontendConfiguration.ts"] -14["highAvailability.ts"] -end -subgraph 8["data"] -9["variables.json"] -end -subgraph D["middleware"] -E["authMiddleware.ts"] -H["checkLock.ts"] -I["rateLimiter.ts"] -end -subgraph F["handlers"] -G["response.ts"] -M["auth.ts"] -Q["data.ts"] -T["frontend.ts"] -X["api.ts"] -10["graph.ts"] -13["ha.ts"] -19["notification.ts"] -1C["conf.ts"] -end -subgraph J["routes"] -subgraph K["auth"] -L["routes.ts"] -end -subgraph O["data"] -P["routes.ts"] -end -subgraph R["frontendController"] -S["routes.ts"] -end -subgraph V["getter"] -W["routes.ts"] -end -subgraph Y["graphs"] -Z["routes.ts"] -end -subgraph 11["highavailability"] -12["routes.ts"] -end -subgraph 17["notifications"] -18["routes.ts"] -end -subgraph 1A["setter"] -1B["routes.ts"] -end -end -subgraph 15["typings"] -16["ha.ts"] -end -end -1-->2 -2-->4 -2-->6 -2-->A -2-->E -2-->H -2-->I -2-->L -2-->P -2-->S -2-->W -2-->Z -2-->12 -2-->18 -2-->1B -6-->7 -7-->9 -A-->B -A-->C -C-->B -E-->G -H-->G -L-->M -M-->N -M-->G -P-->Q -Q-->B -Q-->G -S-->T -T-->U -T-->G -W-->X -X-->A -X-->G -Z-->10 -Z-->G -12-->13 -13-->14 -13-->G -14-->7 -14-->16 -18-->19 -19-->G -1B-->1C -1C-->A -1C-->G diff --git a/src/misc/dependencyGraphs/mermaid-api.txt b/src/misc/dependencyGraphs/mermaid-api.txt deleted file mode 100644 index 3cb4811..0000000 --- a/src/misc/dependencyGraphs/mermaid-api.txt +++ /dev/null @@ -1,26 +0,0 @@ -flowchart TB - -subgraph 0["src"] -subgraph 1["routes"] -subgraph 2["getter"] -3["routes.ts"] -end -end -subgraph 4["handlers"] -5["api.ts"] -B["response.ts"] -end -subgraph 6["controllers"] -7["scheduler.ts"] -A["fetchData.ts"] -end -subgraph 8["config"] -9["db.ts"] -end -end -3-->5 -5-->7 -5-->B -7-->9 -7-->A -A-->9 diff --git a/src/misc/dependencyGraphs/mermaid-auth.txt b/src/misc/dependencyGraphs/mermaid-auth.txt deleted file mode 100644 index 336dded..0000000 --- a/src/misc/dependencyGraphs/mermaid-auth.txt +++ /dev/null @@ -1,19 +0,0 @@ -flowchart TB - -subgraph 0["src"] -subgraph 1["routes"] -subgraph 2["auth"] -3["routes.ts"] -end -end -subgraph 4["handlers"] -5["auth.ts"] -8["response.ts"] -end -subgraph 6["controllers"] -7["auth.ts"] -end -end -3-->5 -5-->7 -5-->8 diff --git a/src/misc/dependencyGraphs/mermaid-conf.txt b/src/misc/dependencyGraphs/mermaid-conf.txt deleted file mode 100644 index 370dd89..0000000 --- a/src/misc/dependencyGraphs/mermaid-conf.txt +++ /dev/null @@ -1,26 +0,0 @@ -flowchart TB - -subgraph 0["src"] -subgraph 1["routes"] -subgraph 2["setter"] -3["routes.ts"] -end -end -subgraph 4["handlers"] -5["conf.ts"] -B["response.ts"] -end -subgraph 6["controllers"] -7["scheduler.ts"] -A["fetchData.ts"] -end -subgraph 8["config"] -9["db.ts"] -end -end -3-->5 -5-->7 -5-->B -7-->9 -7-->A -A-->9 diff --git a/src/misc/dependencyGraphs/mermaid-data.txt b/src/misc/dependencyGraphs/mermaid-data.txt deleted file mode 100644 index 4aa6a13..0000000 --- a/src/misc/dependencyGraphs/mermaid-data.txt +++ /dev/null @@ -1,19 +0,0 @@ -flowchart TB - -subgraph 0["src"] -subgraph 1["routes"] -subgraph 2["data"] -3["routes.ts"] -end -end -subgraph 4["handlers"] -5["data.ts"] -8["response.ts"] -end -subgraph 6["config"] -7["db.ts"] -end -end -3-->5 -5-->7 -5-->8 diff --git a/src/misc/dependencyGraphs/mermaid-frontend.txt b/src/misc/dependencyGraphs/mermaid-frontend.txt deleted file mode 100644 index 8dde5ce..0000000 --- a/src/misc/dependencyGraphs/mermaid-frontend.txt +++ /dev/null @@ -1,19 +0,0 @@ -flowchart TB - -subgraph 0["src"] -subgraph 1["routes"] -subgraph 2["frontendController"] -3["routes.ts"] -end -end -subgraph 4["handlers"] -5["frontend.ts"] -8["response.ts"] -end -subgraph 6["controllers"] -7["frontendConfiguration.ts"] -end -end -3-->5 -5-->7 -5-->8 diff --git a/src/misc/dependencyGraphs/mermaid-graph.txt b/src/misc/dependencyGraphs/mermaid-graph.txt deleted file mode 100644 index 3448453..0000000 --- a/src/misc/dependencyGraphs/mermaid-graph.txt +++ /dev/null @@ -1,15 +0,0 @@ -flowchart TB - -subgraph 0["src"] -subgraph 1["routes"] -subgraph 2["graphs"] -3["routes.ts"] -end -end -subgraph 4["handlers"] -5["graph.ts"] -6["response.ts"] -end -end -3-->5 -3-->6 diff --git a/src/misc/dependencyGraphs/mermaid-ha.txt b/src/misc/dependencyGraphs/mermaid-ha.txt deleted file mode 100644 index 2c789f6..0000000 --- a/src/misc/dependencyGraphs/mermaid-ha.txt +++ /dev/null @@ -1,31 +0,0 @@ -flowchart TB - -subgraph 0["src"] -subgraph 1["routes"] -subgraph 2["highavailability"] -3["routes.ts"] -end -end -subgraph 4["handlers"] -5["ha.ts"] -E["response.ts"] -end -subgraph 6["controllers"] -7["highAvailability.ts"] -end -subgraph 8["config"] -9["variables.ts"] -end -subgraph A["data"] -B["variables.json"] -end -subgraph C["typings"] -D["ha.ts"] -end -end -3-->5 -5-->7 -5-->E -7-->9 -7-->D -9-->B diff --git a/src/misc/dependencyGraphs/mermaid-notificationService.txt b/src/misc/dependencyGraphs/mermaid-notificationService.txt deleted file mode 100644 index 2bc9731..0000000 --- a/src/misc/dependencyGraphs/mermaid-notificationService.txt +++ /dev/null @@ -1,15 +0,0 @@ -flowchart TB - -subgraph 0["src"] -subgraph 1["routes"] -subgraph 2["notifications"] -3["routes.ts"] -end -end -subgraph 4["handlers"] -5["notification.ts"] -6["response.ts"] -end -end -3-->5 -5-->6 diff --git a/src/misc/entrypoint.sh b/src/misc/entrypoint.sh deleted file mode 100755 index b352ca7..0000000 --- a/src/misc/entrypoint.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/bash - -VERSION="$(cat ./package.json | grep version | cut -d '"' -f 4)" - -if [[ "$1" = "--dev" ]]; then - node_env="development" -elif [[ "$1" = "--prod" ]]; then - node_env="production" -fi - -echo -e " -\033[1;32mWelcome to\033[0m - -\033[1;34m###### ###### #### ### ### #### ######### ###### #########\033[0m -\033[1;34m### ### ### ### ### ### ### ### ### ### ### ###\033[0m -\033[1;34m### ### ### ### ### ###### #### ### ### ### ###\033[0m -\033[1;34m### ### ### ### ### ### ### #### ### ############ ###\033[0m -\033[1;34m### ### ### ### ### ### ### #### ### ### ### ###\033[0m -\033[1;34m###### ###### #### ### ### #### ### ### ### ### \033[0m(\033[1;33mAPI - v${VERSION}\033[0m) - -\033[1;36mUseful links:\033[0m - -- Documentation: \033[1;32mhttps://outline.itsnik.de/s/dockstat\033[0m -- GitHub (Frontend): \033[1;32mhttps://github.com/its4nik/dockstat\033[0m -- GitHub (Backend): \033[1;32mhttps://github.com/its4nik/dockstatapi\033[0m - -\033[1;35mSummary:\033[0m - -DockStat and DockStatAPI are 2 fully OpenSource projects, DockStatAPI is a simple but extensible API which allows queries via a REST endpoint. - -" - -bash ./createEnvFile.sh - -NODE_ENV=${node_env} node src/server.js diff --git a/src/misc/minifyDist.sh b/src/misc/minifyDist.sh deleted file mode 100755 index 171ef09..0000000 --- a/src/misc/minifyDist.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash - -dist="$(pwd)/dist" - -run_script() { - npx uglifyjs --no-annotations --in-situ "$1" > /dev/null - echo "✔️ Minified : $(basename "$1")" -} - -if [ -d "$dist" ]; then - echo "::: Dist directory exists." -else - echo "::: Dist does not exist... Running npx tsc" - npx tsc -fi - -max_jobs=$(nproc) -job_count=0 - -for file in $(find "$dist" -type f -name "*.js"); do - run_script "$file" & - ((job_count++)) - - if ((job_count >= max_jobs)); then - wait - job_count=0 - fi -done - -wait - -echo - -if [[ $1 == "--build-only" ]]; then - exit 0 -fi - -node dist/server.js diff --git a/src/misc/removeUnusedDeps.sh b/src/misc/removeUnusedDeps.sh deleted file mode 100755 index 5e806df..0000000 --- a/src/misc/removeUnusedDeps.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/bash - -echo "Creating unused dependency list" - -TMP="$(npx depcheck --ignores https,@typescript-eslint/eslint-plugin,@typescript-eslint/parser,license-checker,uglify-js,@types/supports-color,ipaddr.js,dependency-cruiser,tsx,@types/bcrypt,@types/express,@types/express-handlebars,@types/node,ts-node --quiet --oneline | tail -n 1 | tr -d '\n')" - -lines=$(echo -n "$TMP" | tr -s ' ' '\n' | wc -l) - -if ((lines == 0)); then - echo "No unused dependencies." -else - echo - echo "Removing these unused dependencies ($lines):" - for entry in $TMP; do - echo "$entry" - done - echo - - - read -n 1 -p "Delete unused dependencies? (y/n) " input - echo - - case $input in - Y|y) - COMMAND=$(echo "npm remove $TMP") - $COMMAND - exit 0 - ;; - *) - echo "Aborting" - exit 1 - ;; - esac -fi - -exit 0 diff --git a/src/plugins/example.plugin.ts b/src/plugins/example.plugin.ts new file mode 100644 index 0000000..48ca11a --- /dev/null +++ b/src/plugins/example.plugin.ts @@ -0,0 +1,11 @@ +import { Plugin } from "~/core/plugins/plugin-manager"; + +export default { + name: "example-plugin", + onContainerStart: (containerInfo) => { + console.log(`Container started: ${containerInfo.id}`); + }, + onMetricsReceived: (metrics) => { + console.log("Received metrics:", metrics); + }, +} satisfies Plugin; diff --git a/src/routes/auth/routes.ts b/src/routes/auth/routes.ts deleted file mode 100644 index 03549bf..0000000 --- a/src/routes/auth/routes.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Router, Request, Response } from "express"; -import { createAuthenticationHandler } from "../../handlers/auth"; - -const router = Router(); - -router.post("/enable", async (req: Request, res: Response): Promise => { - const password = req.query.password as string; - const handler = createAuthenticationHandler(req, res); - await handler.enable(password); -}); - -router.post("/disable", async (req: Request, res: Response): Promise => { - const password = req.query.password as string; - const handler = createAuthenticationHandler(req, res); - await handler.disable(password); -}); - -export default router; diff --git a/src/routes/container-logs.ts b/src/routes/container-logs.ts new file mode 100644 index 0000000..085b19e --- /dev/null +++ b/src/routes/container-logs.ts @@ -0,0 +1,11 @@ +import { Elysia } from "elysia"; + +export const logRoutes = new Elysia({ prefix: "/logs" }).ws("/:containerId", { + open(ws) { + const containerId = ws.data.params.containerId; + console.log(`New log connection for ${containerId}`); + }, + message(ws, message) { + ws.send(message); + }, +}); diff --git a/src/routes/data/routes.ts b/src/routes/data/routes.ts deleted file mode 100644 index 93c4610..0000000 --- a/src/routes/data/routes.ts +++ /dev/null @@ -1,20 +0,0 @@ -import express, { Request, Response } from "express"; -const router = express.Router(); -import { createDatabaseHandler } from "../../handlers/data"; - -router.get("/latest", (req: Request, res: Response) => { - const DatabaseHandler = createDatabaseHandler(req, res); - return DatabaseHandler.latest(); -}); - -router.get("/all", (req: Request, res: Response) => { - const DatabaseHandler = createDatabaseHandler(req, res); - return DatabaseHandler.all(); -}); - -router.delete("/clear", (req: Request, res: Response) => { - const DatabaseHandler = createDatabaseHandler(req, res); - return DatabaseHandler.clear(); -}); - -export default router; diff --git a/src/routes/docker.ts b/src/routes/docker.ts new file mode 100644 index 0000000..993ae38 --- /dev/null +++ b/src/routes/docker.ts @@ -0,0 +1,22 @@ +import { Elysia, t } from "elysia"; +import { dockerHostManager } from "../core/docker/host-manager"; + +export const dockerRoutes = new Elysia({ prefix: "/docker-hosts" }) + .post( + "/", + async ({ body }) => { + const { id, url } = body; + await dockerHostManager.connect(id, url); + return { success: true }; + }, + { + body: t.Object({ + id: t.String(), + url: t.String(), + pollInterval: t.Number(), + }), + }, + ) + .get("/", () => { + return Array.from(dockerHostManager.connections.keys()); + }); diff --git a/src/routes/frontendController/routes.ts b/src/routes/frontendController/routes.ts deleted file mode 100644 index 723afa4..0000000 --- a/src/routes/frontendController/routes.ts +++ /dev/null @@ -1,76 +0,0 @@ -import express from "express"; -const router = express.Router(); -import { createFrontendHandler } from "../../handlers/frontend"; - -router.post("/show/:containerName", async (req, res) => { - const FrontendHandler = createFrontendHandler(req, res); - const containerName = req.params.containerName; - return FrontendHandler.show(containerName); -}); - -router.post("/tag/:containerName/:tag", async (req, res) => { - const { containerName, tag } = req.params; - const FrontendHandler = createFrontendHandler(req, res); - return FrontendHandler.addTag(containerName, tag); -}); - -router.post("/pin/:containerName", async (req, res) => { - const { containerName } = req.params; - const FrontendHandler = createFrontendHandler(req, res); - return FrontendHandler.pin(containerName); -}); - -router.post("/add-link/:containerName/:link", async (req, res) => { - const { containerName, link } = req.params; - const FrontendHandler = createFrontendHandler(req, res); - return FrontendHandler.addLink(containerName, link); -}); - -router.post( - "/add-icon/:containerName/:icon/:useCustomIcon", - async (req, res) => { - const { containerName, icon, useCustomIcon } = req.params; - const FrontendHandler = createFrontendHandler(req, res); - return FrontendHandler.addIcon(containerName, icon, useCustomIcon); - }, -); - -/* - ____ _____ _ _____ _____ _____ -| _ \| ____| | | ____|_ _| ____| -| | | | _| | | | _| | | | _| -| |_| | |___| |___| |___ | | | |___ -|____/|_____|_____|_____| |_| |_____| -*/ - -router.delete("/hide/:containerName", async (req, res) => { - const { containerName } = req.params; - const FrontendHandler = createFrontendHandler(req, res); - return FrontendHandler.hide(containerName); -}); - -router.delete("/remove-tag/:containerName/:tag", async (req, res) => { - const { containerName, tag } = req.params; - const FrontendHandler = createFrontendHandler(req, res); - return FrontendHandler.removeTag(containerName, tag); -}); - -router.delete("/unpin/:containerName", async (req, res) => { - const { containerName } = req.params; - const FrontendHandler = createFrontendHandler(req, res); - return FrontendHandler.unPin(containerName); -}); - -router.delete("/remove-link/:containerName", async (req, res) => { - const { containerName } = req.params; - const FrontendHandler = createFrontendHandler(req, res); - return FrontendHandler.removeLink(containerName); -}); - -router.delete("/remove-icon/:containerName", async (req, res) => { - const { containerName } = req.params; - const FrontendHandler = createFrontendHandler(req, res); - return FrontendHandler.removeIcon(containerName); -}); - -export default router; diff --git a/src/routes/getter/routes.ts b/src/routes/getter/routes.ts deleted file mode 100644 index d08ae51..0000000 --- a/src/routes/getter/routes.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Router, Request, Response } from "express"; -import { createApiHandler } from "../../handlers/api"; -const router = Router(); - -router.get("/hosts", (req: Request, res: Response) => { - const ApiHandler = createApiHandler(req, res); - return ApiHandler.hosts(); -}); - -router.get("/system", (req: Request, res: Response) => { - const ApiHandler = createApiHandler(req, res); - return ApiHandler.system(); -}); - -router.get("/host/:hostName/stats", async (req: Request, res: Response) => { - const { hostName } = req.params; - const ApiHandler = createApiHandler(req, res); - return ApiHandler.hostStats(hostName); -}); - -router.get("/containers", async (req: Request, res: Response) => { - const ApiHandler = createApiHandler(req, res); - return ApiHandler.containers(); -}); - -router.get("/config", async (req: Request, res: Response) => { - const ApiHandler = createApiHandler(req, res); - return ApiHandler.config(); -}); - -router.get("/current-schedule", (req: Request, res: Response) => { - const ApiHandler = createApiHandler(req, res); - return ApiHandler.currentSchedule(); -}); - -router.get("/status", async (req: Request, res: Response) => { - const ApiHandler = createApiHandler(req, res); - return ApiHandler.status(); -}); - -router.get("/frontend-config", (req: Request, res: Response) => { - const ApiHandler = createApiHandler(req, res); - return ApiHandler.frontendConfig(); -}); - -export default router; diff --git a/src/routes/graphs/routes.ts b/src/routes/graphs/routes.ts deleted file mode 100644 index fcaa798..0000000 --- a/src/routes/graphs/routes.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Request, Response, Router } from "express"; -import { createResponseHandler } from "../../handlers/response"; -import path from "path"; -import { rateLimitedReadFile } from "../../utils/rateLimitFS"; -const router = Router(); - -router.get("/json", async (req: Request, res: Response) => { - const ResponseHandler = createResponseHandler(res); - try { - const data = await rateLimitedReadFile( - path.join(__dirname, "/../../.." + "/src/data/graph.json"), - ); - return ResponseHandler.rawData(data, "Graph JSON fetched"); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } -}); - -export default router; diff --git a/src/routes/highavailability/routes.ts b/src/routes/highavailability/routes.ts deleted file mode 100644 index d4adc46..0000000 --- a/src/routes/highavailability/routes.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Router, Request, Response } from "express"; -import { SyncRequestBody } from "../../typings/syncRequestBody"; -import { createHaHandler } from "../../handlers/ha"; -const router = Router(); - -router.get("/config", async (req: Request, res: Response) => { - const HaHandler = createHaHandler(req, res); - return HaHandler.config(); -}); - -router.post( - "/sync", - async ( - req: Request<{}, {}, SyncRequestBody>, // eslint-disable-line - res: Response, - ): Promise => { - const HaHandler = createHaHandler(req, res); - return HaHandler.sync(req); - }, -); - -router.get("/prepare-sync", async (req: Request, res: Response) => { - const HaHandler = createHaHandler(req, res); - return HaHandler.prepare(); -}); - -export default router; diff --git a/src/routes/logs.ts b/src/routes/logs.ts new file mode 100644 index 0000000..c416000 --- /dev/null +++ b/src/routes/logs.ts @@ -0,0 +1,30 @@ +import { Elysia } from "elysia"; +import { dbFunctions } from "~/core/database/repository"; +import { logger } from "~/core/utils/logger"; + +export const backendLogs = new Elysia({ prefix: "/logs" }) + .get("/", async ({ set }) => { + try { + const logs = dbFunctions.getAllLogs(); + set.headers["Content-Type"] = "application/json"; + logger.debug(`Retrieved all logs`); + return logs; + } catch (error) { + set.status = 500; + logger.error("Failed to retrieve logs,", error); + return { error: "Failed to retrieve logs" }; + } + }) + + .get("/:level", async ({ params: { level }, set }) => { + try { + const logs = dbFunctions.getLogsByLevel(level); + set.headers["Content-Type"] = "application/json"; + logger.debug(`Retrieved logs (level: ${level})`); + return logs; + } catch (error) { + set.status = 500; + logger.error("Failed to retrieve logs"); + return { error: "Failed to retrieve logs" }; + } + }); diff --git a/src/routes/notifications/routes.ts b/src/routes/notifications/routes.ts deleted file mode 100644 index 13b754b..0000000 --- a/src/routes/notifications/routes.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Request, Response, Router } from "express"; -import { createNotificationHandler } from "../../handlers/notification"; -const router = Router(); - -router.get("/get-template", (req: Request, res: Response) => { - const NotificationHandler = createNotificationHandler(req, res); - return NotificationHandler.getTemplate(); -}); - -router.post("/set-template", (req: Request, res: Response): void => { - const NotificationHandler = createNotificationHandler(req, res); - return NotificationHandler.setTemplate(req); -}); - -router.post("/test/:type/:containerId", async (req: Request, res: Response) => { - const NotificationHandler = createNotificationHandler(req, res); - NotificationHandler.test(req); -}); - -export default router; diff --git a/src/routes/setter/routes.ts b/src/routes/setter/routes.ts deleted file mode 100644 index 1615029..0000000 --- a/src/routes/setter/routes.ts +++ /dev/null @@ -1,20 +0,0 @@ -import express, { Router, Request, Response } from "express"; -const router: Router = express.Router(); -import { createConfHandler } from "../../handlers/conf"; - -router.put("/addHost", async (req: Request, res: Response): Promise => { - const ConfHandler = createConfHandler(req, res); - return ConfHandler.addHost(req); -}); - -router.delete("/removeHost", (req: Request, res: Response): void => { - const ConfHandler = createConfHandler(req, res); - return ConfHandler.removeHost(req); -}); - -router.put("/scheduler", (req: Request, res: Response) => { - const ConfHandler = createConfHandler(req, res); - return ConfHandler.scheduler(req); -}); - -export default router; diff --git a/src/routes/stack/routes.ts b/src/routes/stack/routes.ts deleted file mode 100644 index 8f9b9ae..0000000 --- a/src/routes/stack/routes.ts +++ /dev/null @@ -1,35 +0,0 @@ -import express, { Router, Request, Response } from "express"; -const router: Router = express.Router(); -import { createStackHandler } from "../../handlers/stack"; - -router.post("/create/:name", async (req: Request, res: Response) => { - const StackHandler = createStackHandler(req, res); - return StackHandler.createStack(req, res); -}); - -router.post("/start/:name", async (req: Request, res: Response) => { - const StackHandler = createStackHandler(req, res); - return StackHandler.start(req, res); -}); - -router.post("/stop/:name", async (req: Request, res: Response) => { - const StackHandler = createStackHandler(req, res); - return StackHandler.stop(req, res); -}); - -router.get("/get/:name", async (req: Request, res: Response) => { - const StackHandler = createStackHandler(req, res); - return await StackHandler.stackCompose(req, res); -}); - -router.post("/set-env/:name", async (req: Request, res: Response) => { - const StackHandler = createStackHandler(req, res); - return await StackHandler.setStackEnv(req, res); -}); - -router.get("/get-env/:name", async (req: Request, res: Response) => { - const StackHandler = createStackHandler(req, res); - return await StackHandler.getStackEnv(req, res); -}); - -export default router; diff --git a/src/sample-variable.json b/src/sample-variable.json deleted file mode 100644 index f507796..0000000 --- a/src/sample-variable.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "VERSION": "", - "RUNNING_IN_DOCKER": "", - "TRUSTED_PROXIES": "", - "HA_MASTER": "", - "HA_MASTER_IP": "", - "HA_NODE": "", - "HA_UNSAFE": "", - "DISCORD_WEBHOOK_URL": "", - "EMAIL_SENDER": "", - "EMAIL_RECIPIENT": "", - "EMAIL_PASSWORD": "", - "EMAIL_SERVICE": "", - "PUSHBULLET_ACCESS_TOKEN": "", - "PUSHOVER_USER_KEY": "", - "PUSHOVER_API_TOKEN": "", - "SLACK_WEBHOOK_URL": "", - "TELEGRAM_BOT_TOKEN": "", - "TELEGRAM_CHAT_ID": "", - "WHATSAPP_API_URL": "", - "WHATSAPP_RECIPIENT": "", - "AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT": "true", - "LOG_LEVEL": "info" -} diff --git a/src/server.ts b/src/server.ts deleted file mode 100644 index edcb2ec..0000000 --- a/src/server.ts +++ /dev/null @@ -1,18 +0,0 @@ -import express from "express"; -import initializeApp from "./init"; -import writeUserConf from "./config/hostsystem"; -import { startServer } from "./utils/startServer"; -import http from "http"; - -const port: number = parseInt(process.env.PORT || "9876"); -const app = express(); -const server = http.createServer(app); - -initializeApp(app, server); - -if (process.env.NODE_ENV !== "testing") { - writeUserConf(port); - startServer(app, server, port); -} - -export default app; \ No newline at end of file diff --git a/src/typings/atomicWrite.ts b/src/typings/atomicWrite.ts deleted file mode 100644 index 1f4bfb4..0000000 --- a/src/typings/atomicWrite.ts +++ /dev/null @@ -1,6 +0,0 @@ -interface AtomicWriteOptions { - mode?: number; - exclusive?: boolean; -} - -export { AtomicWriteOptions }; diff --git a/src/typings/dockerCompose.ts b/src/typings/dockerCompose.ts deleted file mode 100644 index e30f7e0..0000000 --- a/src/typings/dockerCompose.ts +++ /dev/null @@ -1,92 +0,0 @@ -export interface DockerComposeFile { - services: Record; - networks?: Record; - volumes?: Record; -} - -export interface ServiceDefinition { - image?: string; - build?: BuildDefinition; - container_name?: string; - command?: string | string[]; - environment?: Record; - ports?: string[] | PortMapping[]; - volumes?: string[]; - networks?: string[]; - restart?: string; - depends_on?: string[]; - deploy?: DeployDefinition; - env_file?: string[]; -} - -export interface BuildDefinition { - context: string; - dockerfile?: string; - args?: Record; - cache_from?: string[]; - labels?: Record; - target?: string; -} - -export interface PortMapping { - target: number; - published: number; - protocol?: "tcp" | "udp"; - mode?: "host" | "ingress"; -} - -export interface DeployDefinition { - replicas?: number; - resources?: ResourcesDefinition; - restart_policy?: RestartPolicyDefinition; - labels?: Record; - update_config?: UpdateConfigDefinition; -} - -export interface ResourcesDefinition { - limits?: ResourceLimits; - reservations?: ResourceReservations; -} - -export interface ResourceLimits { - cpus?: string; - memory?: string; -} - -export interface ResourceReservations { - cpus?: string; - memory?: string; -} - -export interface RestartPolicyDefinition { - condition?: "none" | "on-failure" | "any"; - delay?: string; - max_attempts?: number; - window?: string; -} - -export interface UpdateConfigDefinition { - parallelism?: number; - delay?: string; - failure_action?: "continue" | "pause"; - monitor?: string; - max_failure_ratio?: number; - order?: "start-first" | "stop-first"; -} - -export interface NetworkDefinition { - driver?: string; - driver_opts?: Record; - attachable?: boolean; - external?: boolean; - internal?: boolean; - labels?: Record; -} - -export interface VolumeDefinition { - driver?: string; - driver_opts?: Record; - external?: boolean; - labels?: Record; - name?: string; -} diff --git a/src/typings/dockerConfig.ts b/src/typings/dockerConfig.ts deleted file mode 100644 index a1749d1..0000000 --- a/src/typings/dockerConfig.ts +++ /dev/null @@ -1,35 +0,0 @@ -interface target { - name: string; - url: string; - port: number; -} - -interface dockerConfig { - hosts: target[]; -} - -interface HostConfig { - name: string; - [key: string]: string | number; -} - -interface ContainerData { - name: string; - id: string; - hostName: string; - state: string; - cpu_usage: number; - mem_usage: number; - mem_limit: number; - net_rx: number; - net_tx: number; - current_net_rx: number; - current_net_tx: number; - networkMode: string; -} - -interface AllContainerData { - [hostName: string]: ContainerData[] | { error: string }; -} - -export { dockerConfig, target, ContainerData, AllContainerData, HostConfig }; diff --git a/src/typings/dockerStackEnv.ts b/src/typings/dockerStackEnv.ts deleted file mode 100644 index c784b85..0000000 --- a/src/typings/dockerStackEnv.ts +++ /dev/null @@ -1,10 +0,0 @@ -interface dockerStackProperty { - name: string; - value: string; -} - -interface dockerStackEnv { - environment: dockerStackProperty[]; -} - -export { dockerStackEnv, dockerStackProperty }; diff --git a/src/typings/frontendConfig.ts b/src/typings/frontendConfig.ts deleted file mode 100644 index 6ce1497..0000000 --- a/src/typings/frontendConfig.ts +++ /dev/null @@ -1,12 +0,0 @@ -interface Container { - name: string; - hidden?: boolean; - tags?: string[]; - link?: string; - icon?: string; - pinned?: boolean; -} - -type FrontendConfig = Container[]; - -export { FrontendConfig }; diff --git a/src/typings/ha.ts b/src/typings/ha.ts deleted file mode 100644 index f0352fc..0000000 --- a/src/typings/ha.ts +++ /dev/null @@ -1,20 +0,0 @@ -interface HighAvailabilityConfig { - active: boolean; - master: boolean; - nodes: string[]; -} - -interface Node { - ip: string; - port: number; -} - -interface HaNodeConfig { - master: string; -} - -interface NodeCache { - [nodes: string]: Node; -} - -export { HighAvailabilityConfig, Node, HaNodeConfig, NodeCache }; diff --git a/src/typings/hostData.ts b/src/typings/hostData.ts deleted file mode 100644 index cf5a78d..0000000 --- a/src/typings/hostData.ts +++ /dev/null @@ -1,26 +0,0 @@ -interface Component { - Name: string; - Version: string; -} - -interface JsonData { - hostName: string; - info: { - ID: string; - Containers: number; - ContainersRunning: number; - ContainersPaused: number; - ContainersStopped: number; - Images: number; - OperatingSystem: string; - KernelVersion: string; - Architecture: string; - MemTotal: number; - NCPU: number; - }; - version: { - Components: Component[]; - }; -} - -export { JsonData }; diff --git a/src/typings/response.ts b/src/typings/response.ts deleted file mode 100644 index b122dfe..0000000 --- a/src/typings/response.ts +++ /dev/null @@ -1,6 +0,0 @@ -interface StatusResponse { - ApiReachable: boolean; - online: { [key: string]: boolean }; -} - -export { StatusResponse }; diff --git a/src/typings/stackConfig.ts b/src/typings/stackConfig.ts deleted file mode 100644 index 45c7255..0000000 --- a/src/typings/stackConfig.ts +++ /dev/null @@ -1,5 +0,0 @@ -interface stackConfig { - stacks: string[]; -} - -export { stackConfig }; diff --git a/src/typings/states.ts b/src/typings/states.ts deleted file mode 100644 index d5eed20..0000000 --- a/src/typings/states.ts +++ /dev/null @@ -1,10 +0,0 @@ -interface Container { - name: string; - id: string; - state: string; - hostName: string; -} - -type ContainerStates = Container[]; - -export { ContainerStates, Container }; diff --git a/src/typings/syncRequestBody.ts b/src/typings/syncRequestBody.ts deleted file mode 100644 index 36fd70a..0000000 --- a/src/typings/syncRequestBody.ts +++ /dev/null @@ -1,5 +0,0 @@ -interface SyncRequestBody { - files: Record; -} - -export { SyncRequestBody }; diff --git a/src/typings/table.ts b/src/typings/table.ts deleted file mode 100644 index cf0c18a..0000000 --- a/src/typings/table.ts +++ /dev/null @@ -1,11 +0,0 @@ -type Table = { - id: number; // Primary key, auto-incremented - info: string; // Non-null text field - timestamp: string; // ISO 8601 formatted datetime string -}; - -interface DataRow { - info: string; -} - -export { Table, DataRow }; diff --git a/src/typings/template.ts b/src/typings/template.ts deleted file mode 100644 index 71e0c8a..0000000 --- a/src/typings/template.ts +++ /dev/null @@ -1,5 +0,0 @@ -interface TemplateData { - text: string; -} - -export { TemplateData }; diff --git a/src/utils/assets/api-icon.svg b/src/utils/assets/api-icon.svg deleted file mode 100644 index 5a4fdb7..0000000 --- a/src/utils/assets/api-icon.svg +++ /dev/null @@ -1 +0,0 @@ -\ diff --git a/src/utils/assets/container-icon.svg b/src/utils/assets/container-icon.svg deleted file mode 100644 index 15ed98c..0000000 --- a/src/utils/assets/container-icon.svg +++ /dev/null @@ -1 +0,0 @@ -\ diff --git a/src/utils/assets/server-icon.svg b/src/utils/assets/server-icon.svg deleted file mode 100644 index 31c92d4..0000000 --- a/src/utils/assets/server-icon.svg +++ /dev/null @@ -1 +0,0 @@ -\ diff --git a/src/utils/atomicWrite.ts b/src/utils/atomicWrite.ts deleted file mode 100644 index d279475..0000000 --- a/src/utils/atomicWrite.ts +++ /dev/null @@ -1,35 +0,0 @@ -import fs from "fs"; -import logger from "./logger"; -import { AtomicWriteOptions } from "../typings/atomicWrite"; - -export function atomicWrite( - targetPath: string, - data: object | string | Buffer | Record, - options: AtomicWriteOptions = {}, -): void { - const { mode = 0o600, exclusive = false } = options; - const tempFile = `${targetPath}.tmp`; - - try { - const writeData = - typeof data === "object" && !(data instanceof Buffer) - ? JSON.stringify(data, null, 2) - : data; - - if (exclusive && fs.existsSync(targetPath)) { - throw new Error(`File already exists: ${targetPath}`); - } - - fs.writeFileSync(tempFile, writeData, { mode }); - - fs.renameSync(tempFile, targetPath); - - logger.debug(`File successfully written to: ${targetPath}`); - } catch (error: unknown) { - if (fs.existsSync(tempFile)) fs.unlinkSync(tempFile); - logger.error( - `Failed to write file at ${targetPath}: ${(error as Error).message}`, - ); - throw error; - } -} diff --git a/src/utils/connectionChecker.ts b/src/utils/connectionChecker.ts deleted file mode 100644 index 5a45505..0000000 --- a/src/utils/connectionChecker.ts +++ /dev/null @@ -1,67 +0,0 @@ -import * as fs from "fs"; -import * as net from "net"; -import logger from "./logger"; -import { target } from "../typings/dockerConfig"; -import { StatusResponse } from "../typings/response"; - -const filePath: string = "./src/data/dockerConfig.json"; - -async function checkHostStatus(hosts: target[]): Promise { - const results: { [key: string]: boolean } = {}; - for (const host of hosts) { - const { name, url, port } = host; - - const isOnline = await checkPort(url, port); - - results[name] = !!isOnline; - - if (results[name] == true) { - logger.debug(`${host.url}:${port} is online`); - } else { - logger.debug(`${host.url}:${port} is unreachable`); - } - } - - return { - ApiReachable: true, - online: results, - }; -} - -function checkPort(host: string, port: number): Promise { - return new Promise((resolve) => { - const socket = new net.Socket(); - socket.setTimeout(3000); - - socket.on("connect", () => { - socket.end(); - resolve(true); - }); - - socket.on("timeout", () => { - socket.destroy(); - resolve(false); - }); - - socket.on("error", () => { - socket.destroy(); - resolve(false); - }); - - socket.connect(port, host); - }); -} - -async function checkReachability(): Promise { - try { - const data = fs.readFileSync(filePath, "utf-8"); - const parsedData = JSON.parse(data); - const hosts: target[] = parsedData.hosts; - return await checkHostStatus(hosts); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } -} - -export default checkReachability; diff --git a/src/utils/containerService.ts b/src/utils/containerService.ts deleted file mode 100644 index 0bb0a4e..0000000 --- a/src/utils/containerService.ts +++ /dev/null @@ -1,173 +0,0 @@ -import logger from "./logger"; -import { ContainerInfo } from "dockerode"; -import { getDockerClient } from "./dockerClient"; -import fs from "fs"; -import { atomicWrite } from "./atomicWrite"; -const configPath = "./src/data/dockerConfig.json"; -import { AllContainerData, HostConfig } from "../typings/dockerConfig"; -import { generateGraphJSON } from "../handlers/graph"; -import { WebSocket } from "ws"; - -export function loadConfig() { - try { - if (!fs.existsSync(configPath)) { - logger.warn( - `Config file not found. Creating an empty file at ${configPath}`, - ); - atomicWrite(configPath, JSON.stringify({ hosts: [] }, null, 2)); - } - - const configData = fs.readFileSync(configPath, "utf-8"); - logger.debug("Loaded " + configPath); - return JSON.parse(configData); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - return { hosts: [] }; - } -} - -export async function fetchContainersForHost(hostName: string) { - const config = loadConfig(); - const hostConfig = config.hosts.find((h: HostConfig) => h.name === hostName); - - if (!hostConfig) { - throw new Error(`Host ${hostName} not found in configuration`); - } - - try { - const docker = getDockerClient(hostName); - const containers: ContainerInfo[] = await docker.listContainers({ - all: true, - }); - - return await Promise.all( - containers.map(async (container) => { - try { - const containerInstance = docker.getContainer(container.Id); - const [containerInfo, containerStats] = await Promise.all([ - containerInstance.inspect(), - containerInstance.stats({ stream: false }), - ]); - - const cpuDelta = - containerStats.cpu_stats.cpu_usage.total_usage - - containerStats.precpu_stats.cpu_usage.total_usage; - const systemCpuDelta = - containerStats.cpu_stats.system_cpu_usage - - containerStats.precpu_stats.system_cpu_usage; - const cpuUsage = - systemCpuDelta > 0 - ? (cpuDelta / systemCpuDelta) * - containerStats.cpu_stats.online_cpus - : 0; - - return { - name: container.Names[0].replace("/", ""), - id: container.Id, - hostName, - state: container.State, - cpu_usage: cpuUsage, - mem_usage: containerStats.memory_stats.usage, - mem_limit: containerStats.memory_stats.limit, - net_rx: containerStats.networks?.eth0?.rx_bytes || 0, - net_tx: containerStats.networks?.eth0?.tx_bytes || 0, - current_net_rx: containerStats.networks?.eth0?.rx_bytes || 0, - current_net_tx: containerStats.networks?.eth0?.tx_bytes || 0, - networkMode: containerInfo.HostConfig.NetworkMode || "unknown", - }; - } catch (error) { - logger.error(`Error processing container ${container.Id}: ${error}`); - return { - name: container.Names[0].replace("/", ""), - id: container.Id, - hostName, - state: container.State, - cpu_usage: 0, - mem_usage: 0, - mem_limit: 0, - net_rx: 0, - net_tx: 0, - current_net_rx: 0, - current_net_tx: 0, - networkMode: "unknown", - }; - } - }), - ); - } catch (error) { - logger.error(`Error fetching containers for ${hostName}: ${error}`); - throw error; - } -} - -export async function fetchAllContainers(): Promise { - const config = loadConfig(); - const allContainerData: AllContainerData = {}; - - await Promise.all( - config.hosts.map(async (hostConfig: HostConfig) => { - try { - allContainerData[hostConfig.name] = await fetchContainersForHost( - hostConfig.name, - ); - } catch (error) { - allContainerData[hostConfig.name] = { - error: `Error fetching containers: ${error instanceof Error ? error.message : String(error)}`, - }; - } - }), - ); - - generateGraphJSON(allContainerData); - return allContainerData; -} - -export async function streamContainerData(ws: WebSocket, hostName: string) { - try { - const containers = await fetchContainersForHost(hostName); - ws.send(JSON.stringify({ type: "containers", data: containers })); - - const docker = getDockerClient(hostName); - const eventStream = await docker.getEvents(); - - // eslint-disable-next-line - if (!(eventStream instanceof require("stream").Readable)) { - throw new Error("Failed to get valid event stream"); - } - - const handleData = (chunk: Buffer) => { - ws.send( - JSON.stringify({ type: "container-event", data: chunk.toString() }), - ); - }; - - const handleError = (err: Error) => { - logger.error(`Event stream error for ${hostName}: ${err.message}`); - ws.close(); - }; - - eventStream.on("data", handleData).on("error", handleError); - - const closeHandler = () => { - eventStream - .removeListener("data", handleData) - .removeListener("error", handleError) - .removeListener("closed", handleError); - logger.info(`Closed event stream for ${hostName}`); - }; - - ws.on("close", closeHandler); - ws.on("error", closeHandler); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - logger.error("Container data error:", message); - ws.send( - JSON.stringify({ - error: "Failed to fetch container data", - details: message, - }), - ); - ws.close(); - } -} diff --git a/src/utils/dockerClient.ts b/src/utils/dockerClient.ts deleted file mode 100644 index ff77088..0000000 --- a/src/utils/dockerClient.ts +++ /dev/null @@ -1,41 +0,0 @@ -import Docker from "dockerode"; -import fs from "fs"; -import logger from "./logger"; -import { dockerConfig, target } from "../typings/dockerConfig"; - -function loadDockerConfig(): dockerConfig { - const configPath = "./src/data/dockerConfig.json"; - try { - const rawData = fs.readFileSync(configPath, "utf-8"); - logger.debug("Refreshed DockerConfig.json"); - return JSON.parse(rawData) as dockerConfig; - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - throw new Error(errorMsg); - } -} - -function createDockerClient(hostConfig: target): Docker { - logger.info( - `Creating Docker client for host: ${hostConfig.url} on port: ${hostConfig.port || 2375}`, - ); - return new Docker({ - host: hostConfig.url, - port: hostConfig.port || 2375, - protocol: "http", - }); -} - -export const getDockerClient = (hostName: string): Docker => { - logger.debug(`Getting Docker Client for ${hostName}`); - const config = loadDockerConfig(); - const hostConfig = config.hosts.find((host) => host.name === hostName); - - if (!hostConfig) { - const errorMsg = `Docker host ${hostName} not found in configuration`; - logger.error(errorMsg); - throw new Error(errorMsg); - } - return createDockerClient(hostConfig); -}; diff --git a/src/utils/extractHostData.ts b/src/utils/extractHostData.ts deleted file mode 100644 index 992f963..0000000 --- a/src/utils/extractHostData.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { JsonData } from "../typings/hostData"; -import logger from "./logger"; - -type ComponentMap = Record; - -interface RelevantData { - hostName: string; - info: { - ID: string; - Containers: number; - ContainersRunning: number; - ContainersPaused: number; - ContainersStopped: number; - Images: number; - OperatingSystem: string; - KernelVersion: string; - Architecture: string; - MemTotal: number; - NCPU: number; - }; - version: { - Components: ComponentMap; - }; -} - -function processComponents(components: unknown): ComponentMap { - try { - if (!Array.isArray(components)) return {}; - - return components.reduce((acc, component) => { - if ( - typeof component === "object" && - component !== null && - "Name" in component && - "Version" in component - ) { - const { Name, Version } = component; - if (typeof Name === "string" && typeof Version === "string") { - acc[Name] = Version; - } - } - return acc; - }, {}); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - logger.error(`Error processing components: ${errorMessage}`); - return {}; - } -} - -export function extractRelevantData(jsonData: JsonData): RelevantData { - return { - hostName: jsonData.hostName, - info: { - ID: jsonData.info.ID, - Containers: jsonData.info.Containers, - ContainersRunning: jsonData.info.ContainersRunning, - ContainersPaused: jsonData.info.ContainersPaused, - ContainersStopped: jsonData.info.ContainersStopped, - Images: jsonData.info.Images, - OperatingSystem: jsonData.info.OperatingSystem, - KernelVersion: jsonData.info.KernelVersion, - Architecture: jsonData.info.Architecture, - MemTotal: jsonData.info.MemTotal, - NCPU: jsonData.info.NCPU, - }, - version: { - Components: processComponents(jsonData?.version?.Components), - }, - }; -} - -export default extractRelevantData; diff --git a/src/utils/logger.ts b/src/utils/logger.ts deleted file mode 100644 index 2fd67bd..0000000 --- a/src/utils/logger.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { createLogger, format, transports } from "winston"; -import DailyRotateFile from "winston-daily-rotate-file"; -import { LOG_LEVEL } from "../config/variables"; - -const colors = { - gray: "\x1b[90m", - reset: "\x1b[0m", - white: "\x1b[97m", - red: "\x1b[31m", - green: "\x1b[32m", - yellow: "\x1b[33m", - blue: "\x1b[34m", -}; - -function colorizeLogLevel(level: string, levelName: string) { - switch (level) { - case "info": - return `${colors.green}${levelName}${colors.reset}`; - case "debug": - return `${colors.blue}${levelName}${colors.reset}`; - case "error": - return `${colors.red}${levelName}${colors.reset}`; - case "warn": - return `${colors.yellow}${levelName}${colors.reset}`; - default: - return `${colors.gray}UNKNOWN${colors.reset}`; - } -} - -// Filter out Exit listeners logs -const filterLogs = format((info) => { - if ( - typeof info.message === "string" && - info.message.includes("Exit listeners detected") - ) { - return false; - } - return info; -}); - -const logger = createLogger({ - level: LOG_LEVEL, - format: format.combine( - filterLogs(), - format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), - ), - transports: [ - new transports.Console({ - format: format.combine( - format.printf((info) => { - const level = info.level.toUpperCase().padEnd(5, " "); - const timestamp = `${colors.gray}${info.timestamp}${colors.reset}`; - const levelColorized = colorizeLogLevel( - info.level.toLowerCase(), - level, - ); - const message = `${colors.white}${(info.message as string).replace(/\n|\r/g, "")}${colors.reset}`; - - return `${timestamp} ${levelColorized} : ${message}`; - }), - ), - }), - new DailyRotateFile({ - filename: "logs/app-%DATE%.log", - datePattern: "YYYY-MM-DD", - maxSize: "20m", - maxFiles: "14d", - zippedArchive: true, - format: format.combine( - format.printf((info) => { - const level = info.level.toUpperCase().padEnd(5, " "); - return `${info.timestamp} ${level} : ${info.message}`; - }), - ), - }), - ], -}); - -export default logger; diff --git a/src/utils/notifications/_notify.ts b/src/utils/notifications/_notify.ts deleted file mode 100644 index 49717f9..0000000 --- a/src/utils/notifications/_notify.ts +++ /dev/null @@ -1,51 +0,0 @@ -import logger from "../../utils/logger"; -import { telegramNotification } from "./telegram"; -import { slackNotification } from "./slack"; -import { discordNotification } from "./discord"; -import { emailNotification } from "./email"; -import { whatsappNotification } from "./whatsapp"; -import { pushbulletNotification } from "./pushbullet"; -import { pushoverNotification } from "./pushover"; - -async function notify(type: string, containerId: string) { - if (!containerId) { - logger.error("Container ID is required."); - throw new Error("Container ID is required."); - } - - switch (type) { - case "telegram": - logger.debug("Sending Telegram notification..."); - await telegramNotification(containerId); - break; - case "slack": - logger.debug("Sending Slack notification..."); - await slackNotification(containerId); - break; - case "discord": - logger.debug("Sending Discord notification..."); - await discordNotification(containerId); - break; - case "email": - logger.debug("Sending Email notification..."); - await emailNotification(containerId); - break; - case "whatsapp": - logger.debug("Sending WhatsApp notification..."); - await whatsappNotification(containerId); - break; - case "pushbullet": - logger.debug("Sending Pushbullet notification..."); - await pushbulletNotification(containerId); - break; - case "pushover": - logger.debug("Sending Pushover notification..."); - await pushoverNotification(containerId); - break; - default: - logger.error("Unknown notification type."); - throw new Error("Unknown notification type."); - } -} - -export default notify; diff --git a/src/utils/notifications/_template.ts b/src/utils/notifications/_template.ts deleted file mode 100644 index fd5d71e..0000000 --- a/src/utils/notifications/_template.ts +++ /dev/null @@ -1,76 +0,0 @@ -import fs from "fs"; -import logger from "../logger"; -import { ContainerStates, Container } from "../../typings/states"; - -const templatePath: string = "./src/data/template.json"; -const containersPath: string = "./src/data/states.json"; - -interface Template { - text: string; -} - -function getTemplate(): Template | null { - try { - const data = fs.readFileSync(templatePath, "utf8"); - return JSON.parse(data); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - return null; - } -} - -function setTemplate(newTemplate: string): void { - try { - fs.writeFileSync( - templatePath, - JSON.stringify({ text: newTemplate }, null, 2), - "utf8", - ); - logger.debug("Template updated successfully"); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } -} - -function renderTemplate(containerId: string): string | null { - const template = getTemplate(); - if (!template) { - logger.error("Template is missing or not a string"); - return null; - } - - try { - const data = fs.readFileSync(containersPath, "utf8"); - const containers = JSON.parse(data); - - let containerData: ContainerStates | null = null; - for (const host in containers) { - containerData = containers[host].find( - (c: Container) => c.id === containerId, - ); - if (containerData) { - break; - } - } - - if (!containerData) { - logger.error(`Container with ID ${containerId} not found`); - return null; - } - - // Substitute placeholders in the template with container data - return Object.keys(containerData).reduce((text, key) => { - const value = containerData[key as keyof ContainerStates]; - // Convert value to a string to avoid errors - return text.replace(new RegExp(`{{${key}}}`, "g"), String(value)); - }, template.text); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - return null; - } -} - -export { getTemplate, setTemplate, renderTemplate }; diff --git a/src/utils/notifications/discord.ts b/src/utils/notifications/discord.ts deleted file mode 100644 index d9be3a0..0000000 --- a/src/utils/notifications/discord.ts +++ /dev/null @@ -1,56 +0,0 @@ -import * as https from "https"; -import logger from "../logger"; -import { renderTemplate } from "./_template"; -import { DISCORD_WEBHOOK_URL } from "../../config/variables"; - -const discord_webhook_url: string = DISCORD_WEBHOOK_URL; - -export async function discordNotification(containerId: string): Promise { - const discord_message: string | null = renderTemplate(containerId); - if (!discord_message) { - logger.error("Failed to create notification message."); - return; - } - - if (!discord_webhook_url) { - logger.error("Discord webhook URL is not set."); - return; - } - - const postData = JSON.stringify({ - content: discord_message, - }); - - const url = new URL(discord_webhook_url); - - const options = { - hostname: url.hostname, - path: url.pathname, - method: "POST", - headers: { - "Content-Type": "application/json", - "Content-Length": Buffer.byteLength(postData), - }, - }; - - const req = https.request(options, (res) => { - let data = ""; - - res.on("data", (chunk) => { - data += chunk; - }); - - res.on("end", () => { - if (res.statusCode !== 200) { - logger.error(`Discord API error: ${data}`); - } - }); - }); - - req.on("error", (error) => { - logger.error("Error sending Discord message:", error); - }); - - req.write(postData); - req.end(); -} diff --git a/src/utils/notifications/email.ts b/src/utils/notifications/email.ts deleted file mode 100644 index 62b37d3..0000000 --- a/src/utils/notifications/email.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { SendMailOptions, createTransport } from "nodemailer"; -import logger from "../logger"; -import { renderTemplate } from "./_template"; -import { - EMAIL_SENDER, - EMAIL_SERVICE, - EMAIL_PASSWORD, - EMAIL_RECIPIENT, -} from "../../config/variables"; - -const email_sender: string = EMAIL_SENDER; -const email_recipient: string = EMAIL_RECIPIENT; -const email_password: string = EMAIL_PASSWORD; -const email_service: string = EMAIL_SERVICE; - -export async function emailNotification(containerId: string) { - // Validate email configuration parameters - if (!email_sender || !email_recipient || !email_password || !email_service) { - logger.error( - "Email notification failed: Missing configuration parameters. " + - "Please ensure EMAIL_SENDER, EMAIL_RECIPIENT, EMAIL_PASSWORD, and EMAIL_SERVICE are set in environment variables.", - ); - return; - } - - const email_message: string | null = renderTemplate(containerId); - if (!email_message) { - logger.error("Failed to create notification message."); - return; - } - - const transporter = createTransport({ - service: email_service, - auth: { - user: email_sender, - pass: email_password, - }, - }); - - const mailOptions: SendMailOptions = { - from: email_sender, - to: email_recipient, - subject: "DockStat", - text: email_message, - }; - - try { - await transporter.sendMail(mailOptions); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } -} diff --git a/src/utils/notifications/pushbullet.ts b/src/utils/notifications/pushbullet.ts deleted file mode 100644 index 811427a..0000000 --- a/src/utils/notifications/pushbullet.ts +++ /dev/null @@ -1,59 +0,0 @@ -import * as https from "https"; -import logger from "../logger"; -import { renderTemplate } from "./_template"; -import { PUSHBULLET_ACCESS_TOKEN } from "../../config/variables"; - -const pushbullet_access_token: string = PUSHBULLET_ACCESS_TOKEN; - -export async function pushbulletNotification( - containerId: string, -): Promise { - const pushbullet_message: string | null = renderTemplate(containerId); - if (!pushbullet_message) { - logger.error("Failed to create notification message."); - return; - } - - if (!pushbullet_access_token) { - logger.error("Pushbullet access token is not set."); - return; - } - - const postData = JSON.stringify({ - type: "note", - title: "Container Notification", - body: pushbullet_message, - }); - - const options = { - hostname: "api.pushbullet.com", - path: "/v2/pushes", - method: "POST", - headers: { - "Access-Token": pushbullet_access_token, - "Content-Type": "application/json", - "Content-Length": Buffer.byteLength(postData), - }, - }; - - const req = https.request(options, (res) => { - let data = ""; - - res.on("data", (chunk) => { - data += chunk; - }); - - res.on("end", () => { - if (res.statusCode !== 200) { - logger.error(`Pushbullet API error: ${data}`); - } - }); - }); - - req.on("error", (error) => { - logger.error("Error sending Pushbullet message:", error); - }); - - req.write(postData); - req.end(); -} diff --git a/src/utils/notifications/pushover.ts b/src/utils/notifications/pushover.ts deleted file mode 100644 index aac71b3..0000000 --- a/src/utils/notifications/pushover.ts +++ /dev/null @@ -1,57 +0,0 @@ -import * as https from "https"; -import logger from "../logger"; -import { renderTemplate } from "./_template"; -import { PUSHOVER_USER_KEY, PUSHOVER_API_TOKEN } from "../../config/variables"; - -const pushover_user_key: string = PUSHOVER_USER_KEY; -const pushover_api_token: string = PUSHOVER_API_TOKEN; - -export async function pushoverNotification(containerId: string): Promise { - const pushover_message: string | null = renderTemplate(containerId); - if (!pushover_message) { - logger.error("Failed to create notification message."); - return; - } - - if (!pushover_api_token || !pushover_user_key) { - logger.error("Pushover API token or user key is not set."); - return; - } - - const postData = new URLSearchParams({ - token: pushover_api_token, - user: pushover_user_key, - message: pushover_message, - }).toString(); - - const options = { - hostname: "api.pushover.net", - path: "/1/messages.json", - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - "Content-Length": Buffer.byteLength(postData), - }, - }; - - const req = https.request(options, (res) => { - let data = ""; - - res.on("data", (chunk) => { - data += chunk; - }); - - res.on("end", () => { - if (res.statusCode !== 200) { - logger.error(`Pushover API error: ${data}`); - } - }); - }); - - req.on("error", (error) => { - logger.error("Error sending Pushover message:", error); - }); - - req.write(postData); - req.end(); -} diff --git a/src/utils/notifications/slack.ts b/src/utils/notifications/slack.ts deleted file mode 100644 index e1e7216..0000000 --- a/src/utils/notifications/slack.ts +++ /dev/null @@ -1,56 +0,0 @@ -import * as https from "https"; -import logger from "../logger"; -import { renderTemplate } from "./_template"; -import { SLACK_WEBHOOK_URL } from "../../config/variables"; - -const slack_webhook_url: string = SLACK_WEBHOOK_URL; - -export async function slackNotification(containerId: string): Promise { - const slack_message: string | null = renderTemplate(containerId); - if (!slack_message) { - logger.error("Failed to create notification message."); - return; - } - - if (!slack_webhook_url) { - logger.error("Slack webhook URL is not set."); - return; - } - - const postData = JSON.stringify({ - text: slack_message, - }); - - const url = new URL(slack_webhook_url); - - const options = { - hostname: url.hostname, - path: url.pathname, - method: "POST", - headers: { - "Content-Type": "application/json", - "Content-Length": Buffer.byteLength(postData), - }, - }; - - const req = https.request(options, (res) => { - let data = ""; - - res.on("data", (chunk) => { - data += chunk; - }); - - res.on("end", () => { - if (res.statusCode !== 200) { - logger.error(`Slack API error: ${data}`); - } - }); - }); - - req.on("error", (error) => { - logger.error("Error sending Slack message:", error); - }); - - req.write(postData); - req.end(); -} diff --git a/src/utils/notifications/telegram.ts b/src/utils/notifications/telegram.ts deleted file mode 100644 index 440e091..0000000 --- a/src/utils/notifications/telegram.ts +++ /dev/null @@ -1,56 +0,0 @@ -import * as https from "https"; -import logger from "../logger"; -import { renderTemplate } from "./_template"; -import { TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID } from "../../config/variables"; - -const telegram_bot_token: string = TELEGRAM_BOT_TOKEN; -const telegram_chat_id: string = TELEGRAM_CHAT_ID; - -export async function telegramNotification(containerId: string): Promise { - const telegram_message: string | null = renderTemplate(containerId); - if (!telegram_message) { - logger.error("Failed to create notification message."); - return; - } - - if (!telegram_bot_token || !telegram_chat_id) { - logger.error("Telegram bot token or chat ID is not set."); - return; - } - - const postData = JSON.stringify({ - chat_id: telegram_chat_id, - text: telegram_message, - }); - - const options = { - hostname: "api.telegram.org", - path: `/bot${telegram_bot_token}/sendMessage`, - method: "POST", - headers: { - "Content-Type": "application/json", - "Content-Length": Buffer.byteLength(postData), - }, - }; - - const req = https.request(options, (res) => { - let data = ""; - - res.on("data", (chunk) => { - data += chunk; - }); - - res.on("end", () => { - if (res.statusCode !== 200) { - logger.error(`Telegram API error: ${data}`); - } - }); - }); - - req.on("error", (error) => { - logger.error("Error sending message:", error); - }); - - req.write(postData); - req.end(); -} diff --git a/src/utils/notifications/whatsapp.ts b/src/utils/notifications/whatsapp.ts deleted file mode 100644 index 1eb7575..0000000 --- a/src/utils/notifications/whatsapp.ts +++ /dev/null @@ -1,58 +0,0 @@ -import * as https from "https"; -import logger from "../logger"; -import { renderTemplate } from "./_template"; -import { WHATSAPP_API_URL, WHATSAPP_RECIPIENT } from "../../config/variables"; - -const whatsapp_api_url: string = WHATSAPP_API_URL; -const whatsapp_recipient: string = WHATSAPP_RECIPIENT; - -export async function whatsappNotification(containerId: string): Promise { - const whatsapp_message: string | null = renderTemplate(containerId); - if (!whatsapp_message) { - logger.error("Failed to create notification message."); - return; - } - - if (!whatsapp_api_url || !whatsapp_recipient) { - logger.error("WhatsApp API URL or recipient is not set."); - return; - } - - const postData = JSON.stringify({ - to: whatsapp_recipient, - body: whatsapp_message, - }); - - const url = new URL(whatsapp_api_url); - - const options = { - hostname: url.hostname, - path: url.pathname, - method: "POST", - headers: { - "Content-Type": "application/json", - "Content-Length": Buffer.byteLength(postData), - }, - }; - - const req = https.request(options, (res) => { - let data = ""; - - res.on("data", (chunk) => { - data += chunk; - }); - - res.on("end", () => { - if (res.statusCode !== 200) { - logger.error(`WhatsApp API error: ${data}`); - } - }); - }); - - req.on("error", (error) => { - logger.error("Error sending WhatsApp message:", error); - }); - - req.write(postData); - req.end(); -} diff --git a/src/utils/rateLimitFS.ts b/src/utils/rateLimitFS.ts deleted file mode 100644 index a8f0b42..0000000 --- a/src/utils/rateLimitFS.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { promises as fs, existsSync } from "fs"; - -const delay = (ms: number): Promise => - new Promise((resolve) => setTimeout(resolve, ms)); - -let lastOperationTime = 0; -const rateLimitDuration = 500; - -export const rateLimitedReadFile = async ( - filePath: string, - encoding: BufferEncoding = "utf8", -): Promise => { - const now = Date.now(); - const timeSinceLastOperation = now - lastOperationTime; - - if (timeSinceLastOperation < rateLimitDuration) { - await delay(rateLimitDuration - timeSinceLastOperation); - } - - lastOperationTime = Date.now(); - return fs.readFile(filePath, encoding); -}; - -export const rateLimitedExistsSync = async ( - filePath: string, -): Promise => { - const now = Date.now(); - const timeSinceLastOperation = now - lastOperationTime; - - if (timeSinceLastOperation < rateLimitDuration) { - await delay(rateLimitDuration - timeSinceLastOperation); - } - - lastOperationTime = Date.now(); - return existsSync(filePath); -}; diff --git a/src/utils/startServer.ts b/src/utils/startServer.ts deleted file mode 100644 index 52dcc25..0000000 --- a/src/utils/startServer.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Express } from "express"; -import { Server } from "http"; -import { startMasterNode } from "../controllers/highAvailability"; -import writeUserConf from "../config/hostsystem"; -import initFiles from "../config/initFiles"; - -export function startServer(app: Express, server: Server, port: number) { - if (process.env.NODE_ENV === "testing") { - writeUserConf(port); - initFiles(); - } - - server.listen(port, () => { - startMasterNode(); - }); -} diff --git a/src/utils/swaggerDocs.ts b/src/utils/swaggerDocs.ts deleted file mode 100644 index 7ed90d9..0000000 --- a/src/utils/swaggerDocs.ts +++ /dev/null @@ -1,12 +0,0 @@ -import swaggerUi from "swagger-ui-express"; -import { options } from "../config/swaggerConfig"; -import yaml from "yamljs"; -import express from "express"; -import { SwaggerDefinition } from "swagger-jsdoc"; - -const swaggerDocs = (app: express.Application) => { - const swaggerYaml: SwaggerDefinition = yaml.load("./src/config/swagger.yaml"); - app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerYaml, options)); -}; - -export default swaggerDocs; diff --git a/src/utils/webSocket.ts b/src/utils/webSocket.ts deleted file mode 100644 index 66d1f74..0000000 --- a/src/utils/webSocket.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { Server } from "http"; -import { WebSocketServer, WebSocket } from "ws"; -import { URL } from "url"; -import fs from "fs"; -import logger from "./logger"; -import { streamContainerData } from "./containerService"; - -export function setupWebSocket(server: Server) { - const wss = new WebSocketServer({ noServer: true }); - - server.on("upgrade", (req, socket, head) => { - logger.debug(`Received upgrade request for URL: ${req.url}`); - const baseURL = `http://${req.headers.host}/`; - const requestURL = new URL(req.url || "", baseURL); - const { pathname } = requestURL; - logger.debug(`Parsed pathname: ${pathname}`); - - // Debug log to verify path handling - logger.debug(`Handling upgrade for path: ${pathname}`); - - if (pathname === "/wss/container-data" || pathname === "/wss/server-logs") { - wss.handleUpgrade(req, socket, head, (ws) => { - wss.emit("connection", ws, req); - }); - } else { - logger.warn(`Rejected WebSocket connection to invalid path: ${pathname}`); - socket.write("HTTP/1.1 404 Not Found\r\n\r\n"); - socket.destroy(); - } - }); - - server.on("error", (error) => { - logger.error("HTTP server error:", error); - }); - - logger.debug("WebSocket server attached to HTTP server"); - - wss.on("connection", (ws: WebSocket, req) => { - const baseURL = `http://${req.headers.host}/`; - const requestURL = new URL(req.url || "", baseURL); - const { pathname } = requestURL; - - logger.info(`WebSocket connection established to ${pathname}`); - - const handleError = (error: string) => { - ws.send(JSON.stringify({ error })); - ws.close(); - }; - - if (pathname === "/wss/container-data") { - const hostName = requestURL.searchParams.get("host"); - if (!hostName) { - handleError("Missing required host parameter"); - return; - } - streamContainerData(ws, hostName); - } else if (pathname === "/wss/server-logs") { - const logFiles = fs - .readdirSync("logs/") - .filter((file) => file.startsWith("app-")); - - if (logFiles.length === 0) { - console.error("No log files found"); - return; - } - - const sortedLogFiles = logFiles.sort((a, b) => { - const dateA = a.match(/\d{4}-\d{2}-\d{2}/)?.[0] ?? ""; - const dateB = b.match(/\d{4}-\d{2}-\d{2}/)?.[0] ?? ""; - - return dateB.localeCompare(dateA); - }); - - const logPath = "logs/" + sortedLogFiles[0]; - - if (!fs.existsSync(logPath)) { - handleError("Log file not found"); - logger.error(`Log file ${logPath} not found`); - return; - } - - // Read the initial content of the log file - const history = fs.readFileSync(logPath, "utf-8"); - ws.send(JSON.stringify({ type: "log-history", data: history })); - - // Watch the log file for changes - const watcher = fs.watchFile( - logPath, - { interval: 1000 }, - (curr, prev) => { - if (curr.size > prev.size) { - const stream = fs.createReadStream(logPath, { - start: prev.size, - end: curr.size - 1, - encoding: "utf-8", - }); - - stream.on("data", (chunk) => { - ws.send(JSON.stringify({ type: "log-update", data: chunk })); - }); - } - }, - ); - - ws.on("close", () => { - watcher.removeAllListeners(); - logger.info("Closed WebSocket connection for logs"); - }); - } else { - handleError("Invalid WebSocket endpoint"); - } - }); -} diff --git a/tsconfig.json b/tsconfig.json index c4f6f4c..b95e7e0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,17 +1,107 @@ { "compilerOptions": { - "resolveJsonModule": true, - "target": "ES2020", - "outDir": "dist/src", - "module": "CommonJS", - "moduleResolution": "node", - "strict": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "esModuleInterop": true - }, - "$schema": "https://json.schemastore.org/tsconfig", - "display": "Recommended", - "include": ["src/**/*", "**/*.d.ts", "__tests__/**/*"], - "exclude": ["node_modules", "**/*.spec.ts"] + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "ES2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "ES2022" /* Specify what module code is generated. */, + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + "paths": { + "~/*": ["./src/*"] + } /* Specify a set of entries that re-map imports to additional lookup locations. */, + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + "types": [ + "bun-types" + ] /* Specify type package names to be included without being referenced in a source file. */, + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } } From d01c12be4055a4611031f55aadd1271e93199874 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sun, 23 Feb 2025 21:34:05 +0100 Subject: [PATCH 135/324] Feat: Log deletion, first docker statistics, typechecker, and more! --- .dockerignore | 15 ++ bun.lock | 133 +++++++++++++++++ docker/Dockerfile | 3 + docker/docker-compose.dev.yaml | 52 +++++++ package.json | 9 +- src/core/database/repository.ts | 137 ++++++++++++++++-- src/core/utils/logger.ts | 8 +- src/core/utils/type-check.ts | 28 ++++ src/index.ts | 18 +-- src/routes/container-logs.ts | 11 -- src/routes/docker-manager.ts | 42 ++++++ src/routes/docker-stats.ts | 243 ++++++++++++++++++++++++++++++++ src/routes/docker.ts | 22 --- src/routes/logs.ts | 26 ++++ src/typings/docker.ts | 28 ++++ 15 files changed, 712 insertions(+), 63 deletions(-) create mode 100644 .dockerignore create mode 100644 docker/Dockerfile create mode 100644 docker/docker-compose.dev.yaml create mode 100644 src/core/utils/type-check.ts delete mode 100644 src/routes/container-logs.ts create mode 100644 src/routes/docker-manager.ts create mode 100644 src/routes/docker-stats.ts delete mode 100644 src/routes/docker.ts create mode 100644 src/typings/docker.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f965aed --- /dev/null +++ b/.dockerignore @@ -0,0 +1,15 @@ +node_modules +Dockerfile* +docker-compose* +.dockerignore +.git +.gitignore +README.md +LICENSE +.vscode +Makefile +helm-charts +.env +.editorconfig +.idea +coverage* diff --git a/bun.lock b/bun.lock index a5ee6c8..cf94952 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,9 @@ "name": "dockstatapi", "dependencies": { "@elysiajs/swagger": "^1.2.2", + "@types/dockerode": "^3.3.34", "chalk": "^5.4.1", + "dockerode": "^4.0.4", "elysia": "latest", "winston": "^3.17.0", "winston-transport": "^4.9.0", @@ -15,13 +17,44 @@ }, }, }, + "trustedDependencies": [ + "protobufjs", + ], "packages": { + "@balena/dockerignore": ["@balena/dockerignore@1.0.2", "", {}, "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q=="], + "@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="], "@dabh/diagnostics": ["@dabh/diagnostics@2.0.3", "", { "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA=="], "@elysiajs/swagger": ["@elysiajs/swagger@1.2.2", "", { "dependencies": { "@scalar/themes": "^0.9.52", "@scalar/types": "^0.0.12", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.2.0" } }, "sha512-DG0PbX/wzQNQ6kIpFFPCvmkkWTIbNWDS7lVLv3Puy6ONklF14B4NnbDfpYjX1hdSYKeCqKBBOuenh6jKm8tbYA=="], + "@grpc/grpc-js": ["@grpc/grpc-js@1.12.6", "", { "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-JXUj6PI0oqqzTGvKtzOkxtpsyPRNsrmhh41TtIz/zEB6J+AUiZZ0dxWzcMwO9Ns5rmSPuMdghlTbUuqIM48d3Q=="], + + "@grpc/proto-loader": ["@grpc/proto-loader@0.7.13", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.2.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw=="], + + "@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="], + + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], + + "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], + + "@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="], + + "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="], + + "@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="], + + "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], + + "@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="], + + "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], + + "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], + + "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="], + "@scalar/openapi-types": ["@scalar/openapi-types@0.1.1", "", {}, "sha512-NMy3QNk6ytcCoPUGJH0t4NNr36OWXgZhA3ormr3TvhX1NDgoF95wFyodGVH8xiHeUyn2/FxtETm8UBLbB5xEmg=="], "@scalar/themes": ["@scalar/themes@0.9.68", "", { "dependencies": { "@scalar/types": "0.0.34" } }, "sha512-466ac2fdQJOBBSLkGUf88vuZVF+qNMeVpjb0aAHrKkxhpjucTPKdTYO8r2dsX1R5k9A13gWPnm594VW5G/bGHw=="], @@ -30,20 +63,46 @@ "@sinclair/typebox": ["@sinclair/typebox@0.34.27", "", {}, "sha512-C7mxE1VC3WC2McOufZXEU48IfRVI+BcKxk4NOyNn3+JMUNdJHEWGS5CqjuDX+ij2NCCz8/nse1mT7yn8Fv2GHg=="], + "@types/docker-modem": ["@types/docker-modem@3.0.6", "", { "dependencies": { "@types/node": "*", "@types/ssh2": "*" } }, "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg=="], + + "@types/dockerode": ["@types/dockerode@3.3.34", "", { "dependencies": { "@types/docker-modem": "*", "@types/node": "*", "@types/ssh2": "*" } }, "sha512-mH9SuIb8NuTDsMus5epcbTzSbEo52fKLBMo0zapzYIAIyfDqoIFn7L3trekHLKC8qmxGV++pPUP4YqQ9n5v2Zg=="], + "@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], + "@types/ssh2": ["@types/ssh2@1.15.4", "", { "dependencies": { "@types/node": "^18.11.18" } }, "sha512-9JTQgVBWSgq6mAen6PVnrAmty1lqgCMvpfN+1Ck5WRUsyMYPa6qd50/vMJ0y1zkGpOEgLzm8m8Dx/Y5vRouLaA=="], + "@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="], "@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="], "@unhead/schema": ["@unhead/schema@1.11.19", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-7VhYHWK7xHgljdv+C01MepCSYZO2v6OhgsfKWPxRQBDDGfUKCUaChox0XMq3tFvXP6u4zSp6yzcDw2yxCfVMwg=="], + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="], + "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="], + + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + + "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + + "buildcheck": ["buildcheck@0.0.6", "", {}, "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A=="], + "bun-types": ["bun-types@1.2.3", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-P7AeyTseLKAvgaZqQrvp3RqFM3yN9PlcLuSTe7SoJOfZkER73mLdT2vEQi8U64S1YvM/ldcNiQjn0Sn7H9lGgg=="], "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], + "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], + + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + "color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="], "color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], @@ -56,64 +115,138 @@ "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], + "cpu-features": ["cpu-features@0.0.10", "", { "dependencies": { "buildcheck": "~0.0.6", "nan": "^2.19.0" } }, "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA=="], + + "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "docker-modem": ["docker-modem@5.0.6", "", { "dependencies": { "debug": "^4.1.1", "readable-stream": "^3.5.0", "split-ca": "^1.0.1", "ssh2": "^1.15.0" } }, "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ=="], + + "dockerode": ["dockerode@4.0.4", "", { "dependencies": { "@balena/dockerignore": "^1.0.2", "@grpc/grpc-js": "^1.11.1", "@grpc/proto-loader": "^0.7.13", "docker-modem": "^5.0.6", "protobufjs": "^7.3.2", "tar-fs": "~2.0.1", "uuid": "^10.0.0" } }, "sha512-6GYP/EdzEY50HaOxTVTJ2p+mB5xDHTMJhS+UoGrVyS6VC+iQRh7kZ4FRpUYq6nziby7hPqWhOrFFUFTMUZJJ5w=="], + "elysia": ["elysia@1.2.21", "", { "dependencies": { "@sinclair/typebox": "^0.34.27", "cookie": "^1.0.2", "memoirist": "^0.3.0", "openapi-types": "^12.1.3" }, "peerDependencies": { "typescript": ">= 5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-E9b1JcB7fiQ2ptk24W8OnBrMYUoKzffIXob9uTVUKhqOKxaXAd9UyWBeyr7JCDa/VD/b/9S8aIey9/YJsK5sLg=="], + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="], + "end-of-stream": ["end-of-stream@1.4.4", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="], "fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="], + "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], "kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="], + "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], + "logform": ["logform@2.7.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="], + "long": ["long@5.3.1", "", {}, "sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng=="], + "memoirist": ["memoirist@0.3.0", "", {}, "sha512-wR+4chMgVPq+T6OOsk40u9Wlpw1Pjx66NMNiYxCQQ4EUJ7jDs3D9kTCeKdBOkvAiqXlHLVJlvYL01PvIJ1MPNg=="], + "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "nan": ["nan@2.22.1", "", {}, "sha512-pfRR4ZcNTSm2ZFHaztuvbICf+hyiG6ecA06SfAxoPmuHjvMu0KUIae7Y8GyVkbBqeEIidsmXeYooWIX9+qjfRQ=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="], "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], + "protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], + + "pump": ["pump@3.0.2", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw=="], + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], + "split-ca": ["split-ca@1.0.1", "", {}, "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ=="], + + "ssh2": ["ssh2@1.16.0", "", { "dependencies": { "asn1": "^0.2.6", "bcrypt-pbkdf": "^1.0.2" }, "optionalDependencies": { "cpu-features": "~0.0.10", "nan": "^2.20.0" } }, "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg=="], + "stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="], + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "tar-fs": ["tar-fs@2.0.1", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.0.0" } }, "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA=="], + + "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + "text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="], "triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="], + "tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="], + "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + "uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], + "winston": ["winston@3.17.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw=="], "winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="], + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + "zhead": ["zhead@2.2.4", "", {}, "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag=="], "@scalar/themes/@scalar/types": ["@scalar/types@0.0.34", "", { "dependencies": { "@scalar/openapi-types": "0.1.8", "@unhead/schema": "^1.11.11" } }, "sha512-q01ctijmHArM5KOny2zU+sHfhpsgOAENrDENecK2TsQNn5FYLmFZouMKeW2M6F7KFLPZnFxUiL/rT88b6Rp/Kg=="], + "@types/ssh2/@types/node": ["@types/node@18.19.76", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-yvR7Q9LdPz2vGpmpJX5LolrgRdWvB67MJKDPSgIIzpFbaf9a1j/f5DnLp5VDyHGMR0QZHlTr1afsD87QCXFHKw=="], + + "ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + "@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.1.8", "", {}, "sha512-iufA5/6hPCmRIVD2eh7qGpoKvoA08Gw/qUb2JECifBtAwA93fo7+1k9uHK440f2LMJsbxIzA+nv7RS0BmfiO/g=="], + + "@types/ssh2/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], } } diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..fdd4234 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,3 @@ +FROM oven/bun AS base + +WORKDIR /base diff --git a/docker/docker-compose.dev.yaml b/docker/docker-compose.dev.yaml new file mode 100644 index 0000000..39da6d6 --- /dev/null +++ b/docker/docker-compose.dev.yaml @@ -0,0 +1,52 @@ +name: "DockStatAPI - Dev" +services: + socket-proxy: + container_name: Socket-Proxy + image: lscr.io/linuxserver/socket-proxy:latest + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + restart: unless-stopped + read_only: true + tmpfs: + - /run + ports: + - 2375:2375 + environment: + - ALLOW_START=1 #optional + - ALLOW_STOP=1 #optional + - ALLOW_RESTARTS=1 #optional + - AUTH=1 #optional + - BUILD=1 #optional + - COMMIT=1 #optional + - CONFIGS=1 #optional + - CONTAINERS=1 #optional + - DISABLE_IPV6=1 #optional + - DISTRIBUTION=1 #optional + - EVENTS=1 #optional + - EXEC=1 #optional + - IMAGES=1 #optional + - INFO=1 #optional + - NETWORKS=1 #optional + - NODES=1 #optional + - PING=1 #optional + - PLUGINS=1 #optional + - POST=1 #optional + - PROXY_READ_TIMEOUT=240 #optional + - SECRETS=1 #optional + - SERVICES=1 #optional + - SESSION=1 #optional + - SWARM=1 #optional + - SYSTEM=1 #optional + - TASKS=1 #optional + - VERSION=1 #optional + - VOLUMES=1 #optional + + sqlite-web: + container_name: SQLite-web + image: ghcr.io/coleifer/sqlite-web:latest + ports: + - 8080:8080 + volumes: + - ../:/data:ro + environment: + - SQLITE_DATABASE=dockstatapi.db diff --git a/package.json b/package.json index 0e1deb8..d754838 100644 --- a/package.json +++ b/package.json @@ -3,11 +3,13 @@ "version": "2.1.0", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "dev": "bun run --watch src/index.ts" + "dev": "docker compose -f ./docker/docker-compose.dev.yaml up -d && bun run --watch src/index.ts" }, "dependencies": { "@elysiajs/swagger": "^1.2.2", + "@types/dockerode": "^3.3.34", "chalk": "^5.4.1", + "dockerode": "^4.0.4", "elysia": "latest", "winston": "^3.17.0", "winston-transport": "^4.9.0" @@ -15,5 +17,8 @@ "devDependencies": { "bun-types": "latest" }, - "module": "src/index.js" + "module": "src/index.js", + "trustedDependencies": [ + "protobufjs" + ] } diff --git a/src/core/database/repository.ts b/src/core/database/repository.ts index 6d5ea55..6ef778b 100644 --- a/src/core/database/repository.ts +++ b/src/core/database/repository.ts @@ -1,4 +1,6 @@ import Database from "bun:sqlite"; +import { logger } from "~/core/utils/logger"; +import { typeCheck } from "~/core/utils/type-check"; const db = new Database("dockstatapi.db"); @@ -6,7 +8,6 @@ export const dbFunctions = { init() { db.exec(` CREATE TABLE IF NOT EXISTS docker_hosts ( - id TEXT PRIMARY KEY, name TEXT, url TEXT, poll_interval INTEGER @@ -30,11 +31,51 @@ export const dbFunctions = { `); }, - insertMetric(hostId: string, metric: any) { + addDockerHost(hostId: string, url: string, pollInterval: number) { + if ( + !typeCheck(hostId, "string") || + !typeCheck(url, "string") || + !typeCheck(pollInterval, "number") + ) { + logger.crit("Invalid parameter types for addDockerHost"); + throw new TypeError("Invalid parameter types for addDockerHost"); + } + + const stmt = db.prepare(` + INSERT INTO docker_hosts (name, url, poll_interval) + VALUES (?, ?, ?) + `); + return stmt.run(hostId, url, pollInterval); + }, + + getDockerHosts() { const stmt = db.prepare(` - INSERT INTO container_metrics (host_id, container_id, cpu, memory) - VALUES (?, ?, ?, ?) + SELECT name, url, poll_interval + FROM docker_hosts + ORDER BY name DESC `); + return stmt.all(); + }, + + insertMetric(hostId: string, metric: any) { + if (!typeCheck(hostId, "string") || !typeCheck(metric, "object")) { + logger.crit("Invalid parameter types for insertMetric"); + throw new TypeError("Invalid parameter types for insertMetric"); + } + + if ( + !typeCheck(metric.containerId, "string") || + !typeCheck(metric.cpu, "number") || + !typeCheck(metric.memory, "number") + ) { + logger.crit("Invalid metric object structure"); + throw new TypeError("Invalid metric object structure"); + } + + const stmt = db.prepare(` + INSERT INTO container_metrics (host_id, container_id, cpu, memory) + VALUES (?, ?, ?, ?) + `); return stmt.run(hostId, metric.containerId, metric.cpu, metric.memory); }, @@ -44,30 +85,96 @@ export const dbFunctions = { file_name: string, line: number, ) => { + if ( + !typeCheck(level, "string") || + !typeCheck(message, "string") || + !typeCheck(file_name, "string") || + !typeCheck(line, "number") + ) { + logger.crit("Invalid parameter types for addLogEntry"); + throw new TypeError("Invalid parameter types for addLogEntry"); + } + const stmt = db.prepare(` - INSERT INTO backend_log_entries (level, message, file, line) - VALUES (?, ?, ?, ?) - `); + INSERT INTO backend_log_entries (level, message, file, line) + VALUES (?, ?, ?, ?) + `); return stmt.run(level, message, file_name, line); }, getAllLogs() { const stmt = db.prepare(` - SELECT timestamp, level, message, file, line - FROM backend_log_entries - ORDER BY timestamp DESC - `); + SELECT timestamp, level, message, file, line + FROM backend_log_entries + ORDER BY timestamp DESC + `); return stmt.all(); }, getLogsByLevel(level: string) { + if (!typeCheck(level, "string")) { + logger.crit("Level parameter must be a string"); + throw new TypeError("Level parameter must be a string"); + } + + const stmt = db.prepare(` + SELECT timestamp, level, message, file, line + FROM backend_log_entries + WHERE level = ? + ORDER BY timestamp DESC + `); + return stmt.all(level); + }, + + updateDockerHost(name: string, url: string, pollInterval: number) { + if ( + !typeCheck(name, "string") || + !typeCheck(url, "string") || + !typeCheck(pollInterval, "number") + ) { + logger.crit("Invalid parameter types for updateDockerHost"); + throw new TypeError("Invalid parameter types for updateDockerHost"); + } + + const stmt = db.prepare(` + UPDATE docker_hosts + SET url = ?, poll_interval = ? + WHERE name = ? + `); + return stmt.run(url, pollInterval, name); + }, + + deleteDockerHost(name: string) { + if (!typeCheck(name, "string")) { + logger.crit("Invalid parameter type for deleteDockerHost"); + throw new TypeError("Name parameter must be a string"); + } + const stmt = db.prepare(` - SELECT timestamp, level, message, file, line - FROM backend_log_entries + DELETE FROM docker_hosts + WHERE name = ? + `); + return stmt.run(name); + }, + + clearAllLogs() { + const stmt = db.prepare(` + DELETE FROM backend_log_entries + `); + return stmt.run(); + }, + + clearLogsByLevel(level: string) { + if (!typeCheck(level, "string")) { + logger.crit("Invalid parameter type for clearLogsByLevel"); + throw new TypeError("Level parameter must be a string"); + } + + const stmt = db.prepare(` + DELETE FROM backend_log_entries WHERE level = ? - ORDER BY timestamp DESC `); - return stmt.all(level); + return stmt.run(level); }, }; diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts index 076e385..1675d23 100644 --- a/src/core/utils/logger.ts +++ b/src/core/utils/logger.ts @@ -2,7 +2,7 @@ import { createLogger, format, transports } from "winston"; import Transport from "winston-transport"; import path from "path"; import { dbFunctions } from "../database/repository"; -import chalk from "chalk"; +import chalk, { ChalkInstance } from "chalk"; const fileLineFormat = format((info) => { try { @@ -48,7 +48,7 @@ export const logger = createLogger({ new transports.Console({ format: format.combine( format.printf(({ level, message, file, line }) => { - const levelColors: { [key: string]: chalk.Chalk } = { + const levelColors: { [key: string]: ChalkInstance } = { error: chalk.red.bold, warn: chalk.yellow.bold, info: chalk.green.bold, @@ -57,13 +57,13 @@ export const logger = createLogger({ silly: chalk.magenta.bold, }; - const paddedLevel = level.padEnd(5).toUpperCase(); + const paddedLevel = level.toUpperCase(); const coloredLevel = (levelColors[level] || chalk.white)(paddedLevel); const coloredContext = chalk.cyan(`${file}:${line}`); const coloredMessage = chalk.gray(message); - return `[ ${coloredContext.padEnd(22)} ] ${coloredLevel} - ${coloredMessage}`; + return `${coloredLevel} [ ${coloredContext} ] - ${coloredMessage}`; }), ), }), diff --git a/src/core/utils/type-check.ts b/src/core/utils/type-check.ts new file mode 100644 index 0000000..8675f79 --- /dev/null +++ b/src/core/utils/type-check.ts @@ -0,0 +1,28 @@ +type TypeCheck = [any, string]; + +export function typeCheck(value: any, expectedType: string): boolean { + if (expectedType === "null") { + return value === null; + } + + if (expectedType === "array") { + return Array.isArray(value); + } + + const actualType = typeof value; + + if (actualType === "object" && value !== null) { + if (expectedType === "object") { + return !Array.isArray(value); + } + return false; + } + + return actualType === expectedType; +} + +export function validateTypes(checks: TypeCheck[]): boolean[] { + return checks.map(([value, expectedType]) => { + return typeCheck(value, expectedType.toLowerCase()); + }); +} diff --git a/src/index.ts b/src/index.ts index dcca5a9..576d42a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,11 @@ -import { Elysia } from "elysia"; import { swagger } from "@elysiajs/swagger"; -import { loadPlugins } from "~/core/plugins/loader"; -import { dockerRoutes } from "~/routes/docker"; -import { logRoutes } from "~/routes/container-logs"; -import { backendLogs } from "./routes/logs"; +import { Elysia } from "elysia"; import { dbFunctions } from "~/core/database/repository"; +import { loadPlugins } from "~/core/plugins/loader"; import { logger } from "~/core/utils/logger"; +import { dockerRoutes } from "~/routes/docker-manager"; +import { dockerStatsRoutes } from "~/routes/docker-stats"; +import { backendLogs } from "./routes/logs"; dbFunctions.init(); @@ -15,14 +15,14 @@ const app = new Elysia() documentation: { info: { title: "DockStatAPI", - version: "0.1.0", + version: "2.1.0", description: "Docker monitoring API with plugin support", }, }, }), ) .use(dockerRoutes) - .use(logRoutes) + .use(dockerStatsRoutes) .use(backendLogs) .get("/health", () => ({ status: "healthy" })); @@ -31,9 +31,9 @@ async function startServer() { await loadPlugins("./plugins"); app.listen(3000, ({ hostname, port }) => { - logger.info(`🦊 Elysia is running at http://${hostname}:${port}`); + logger.info(`DockStat is running at http://${hostname}:${port}`); logger.info( - `📚 API Documentation available at http://${hostname}:${port}/swagger`, + `Swagger API Documentation available at http://${hostname}:${port}/swagger`, ); }); } catch (error) { diff --git a/src/routes/container-logs.ts b/src/routes/container-logs.ts deleted file mode 100644 index 085b19e..0000000 --- a/src/routes/container-logs.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Elysia } from "elysia"; - -export const logRoutes = new Elysia({ prefix: "/logs" }).ws("/:containerId", { - open(ws) { - const containerId = ws.data.params.containerId; - console.log(`New log connection for ${containerId}`); - }, - message(ws, message) { - ws.send(message); - }, -}); diff --git a/src/routes/docker-manager.ts b/src/routes/docker-manager.ts new file mode 100644 index 0000000..ed2e5ff --- /dev/null +++ b/src/routes/docker-manager.ts @@ -0,0 +1,42 @@ +import { Elysia, t } from "elysia"; +import { dockerHostManager } from "~/core/docker/host-manager"; +import { dbFunctions } from "~/core/database/repository"; +import { logger } from "~/core/utils/logger"; + +export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) + .post( + "/add-host", + async ({ set, body }) => { + try { + const { id, url, pollInterval } = body; + set.headers["Content-Type"] = "application/json"; + dbFunctions.addDockerHost(id, url, pollInterval); + logger.debug(`Added docker host (${id})`); + return { success: true }; + } catch (error) { + set.status = 500; + logger.error("Failed to add host,", error); + return { error: "Failed to add host" }; + } + }, + { + body: t.Object({ + id: t.String(), + url: t.String(), + pollInterval: t.Number(), + }), + }, + ) + + .get("/hosts", async ({ set }) => { + try { + const dockerHosts = dbFunctions.getDockerHosts(); + set.headers["Content-Type"] = "application/json"; + logger.debug("Retrieved docker hosts"); + return dockerHosts; + } catch (error) { + set.status = 500; + logger.error("Failed to retrieve hosts,", error); + return { error: "Failed to retrieve hosts" }; + } + }); diff --git a/src/routes/docker-stats.ts b/src/routes/docker-stats.ts new file mode 100644 index 0000000..5bb9da2 --- /dev/null +++ b/src/routes/docker-stats.ts @@ -0,0 +1,243 @@ +import { Elysia, t } from "elysia"; +import Docker from "dockerode"; +import { dbFunctions } from "~/core/database/repository"; +import { logger } from "~/core/utils/logger"; +import type { HostConfig, DockerHost, ContainerInfo } from "~/typings/docker"; + +interface WsData { + params: any; + interval?: ReturnType; + statsStream?: any; +} + +const getDockerClient = (hostUrl: string): Docker => { + try { + const [host, port] = hostUrl.includes("://") + ? hostUrl.split("://")[1].split(":") + : hostUrl.split(":"); + + const protocol = hostUrl.startsWith("https://") ? "https" : "http"; + + return new Docker({ + protocol, + host, + port: port ? parseInt(port) : protocol === "https" ? 2376 : 2375, + version: "v1.41", + // TODO: Add TLS configuration if needed + }); + } catch (error) { + logger.error("Invalid Docker host URL configuration,", error); + throw new Error("Invalid Docker host configuration"); + } +}; + +export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) + .get("/containers", async ({ set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + const containers: ContainerInfo[] = []; + + await Promise.all( + hosts.map(async (host) => { + try { + const docker = getDockerClient(host.url); + try { + await docker.ping(); + } catch (pingError) { + logger.error("Docker host connection failed,", pingError); + return; + } + + const hostContainers = await docker.listContainers({ all: true }); + + await Promise.all( + hostContainers.map(async (containerInfo) => { + try { + const container = docker.getContainer(containerInfo.Id); + const stats = await new Promise( + (resolve, reject) => { + container.stats({ stream: false }, (err, stats) => { + if (err) { + logger.error("An error occured,", err); + return reject(err); + } + if (!stats) { + logger.error("No stats available"); + return reject(new Error("No stats available")); + } + resolve(stats); + }); + }, + ); + + containers.push({ + id: containerInfo.Id, + hostId: host.name, + name: containerInfo.Names[0].replace(/^\//, ""), + image: containerInfo.Image, + status: containerInfo.Status, + state: containerInfo.State, + cpuUsage: calculateCpuPercent(stats), + memoryUsage: calculateMemoryUsage(stats), + }); + } catch (containerError) { + logger.error( + "Error fetching container stats,", + containerError, + ); + } + }), + ); + logger.debug(`Fetched stats for ${host.name}`); + } catch (hostError) { + logger.error("Error fetching containers for host,", hostError); + } + }), + ); + + set.headers["Content-Type"] = "application/json"; + return { containers }; + } catch (error) { + set.status = 500; + logger.error("Failed to retrieve containers,", error); + return { error: "Failed to retrieve containers" }; + } + }) + + .get("/hosts/:id/config", async ({ params, set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + const host = hosts.find((h) => h.name === params.id); + + if (!host) { + set.status = 404; + logger.error(`Host (${host}) not found`); + return { error: "Host not found" }; + } + + const docker = getDockerClient(host.url); + const info = await docker.info(); + + const config: HostConfig = { + hostId: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + }; + + set.headers["Content-Type"] = "application/json"; + logger.debug(`Fetched config for ${host.name}`); + return config; + } catch (error) { + set.status = 500; + logger.error("Failed to retrieve host config,", error); + return { error: "Failed to retrieve host config" }; + } + }) + + .ws("/hosts/:id/stats", { + message(ws, message) { + ws.send(message); + }, + async open(ws) { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + const host = hosts.find((h) => h.name === ws.data.params.id); + + if (!host) { + ws.close(1008, "Host not found"); + logger.error(`Host (${host}) not found`); + return; + } + + const docker = getDockerClient(host.url); + const interval = setInterval(async () => { + try { + const info = await docker.info(); + ws.send({ + timestamp: Date.now(), + memoryUsage: info.MemTotal - info.MemFree, + cpuUsage: info.NanoCPUs, + containerCount: info.ContainersRunning, + }); + logger.debug(`Fetched host (${host.name}) config`); + } catch (error) { + logger.error("Error fetching host stats,", error); + } + }, 5000); + (ws.data as WsData).interval = interval; + } catch (error) { + logger.error("WebSocket connection failed,", error); + ws.close(1011, "Internal error"); + } + }, + close(ws) { + const data = ws.data as WsData; + if (data.interval) { + clearInterval(data.interval); + } + }, + }) + + .ws("/containers/:hostId/:containerId/metrics", { + message(ws, message) { + ws.send(message); + }, + async open(ws) { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + const host = hosts.find((h) => h.name === ws.data.params.hostId); + + if (!host) { + ws.close(1008, "Host not found"); + logger.error(`Host (${host}) not found`); + return; + } + + const docker = getDockerClient(host.url); + const container = docker.getContainer(ws.data.params.containerId); + const statsStream = await container.stats({ stream: true }); + + statsStream.on("data", (data: Buffer) => { + const stats = JSON.parse(data.toString()); + ws.send({ + cpu: calculateCpuPercent(stats), + memory: calculateMemoryUsage(stats), + timestamp: Date.now(), + }); + }); + + statsStream.on("error", (error) => { + logger.error("Container stats stream error,", error); + ws.close(1011, "Stats stream error"); + }); + + (ws.data as WsData).statsStream = statsStream; + } catch (error) { + logger.error("WebSocket connection failed,", error); + ws.close(1011, "Internal error"); + } + }, + close(ws) { + const data = ws.data as WsData; + if (data.statsStream) { + data.statsStream.destroy(); + } + }, + }); + +const calculateCpuPercent = (stats: Docker.ContainerStats): number => { + const cpuDelta = + stats.cpu_stats.cpu_usage.total_usage - + stats.precpu_stats.cpu_usage.total_usage; + const systemDelta = + stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage; + return (cpuDelta / systemDelta) * 100; +}; + +const calculateMemoryUsage = (stats: Docker.ContainerStats): number => { + return (stats.memory_stats.usage / stats.memory_stats.limit) * 100; +}; diff --git a/src/routes/docker.ts b/src/routes/docker.ts deleted file mode 100644 index 993ae38..0000000 --- a/src/routes/docker.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Elysia, t } from "elysia"; -import { dockerHostManager } from "../core/docker/host-manager"; - -export const dockerRoutes = new Elysia({ prefix: "/docker-hosts" }) - .post( - "/", - async ({ body }) => { - const { id, url } = body; - await dockerHostManager.connect(id, url); - return { success: true }; - }, - { - body: t.Object({ - id: t.String(), - url: t.String(), - pollInterval: t.Number(), - }), - }, - ) - .get("/", () => { - return Array.from(dockerHostManager.connections.keys()); - }); diff --git a/src/routes/logs.ts b/src/routes/logs.ts index c416000..5501f09 100644 --- a/src/routes/logs.ts +++ b/src/routes/logs.ts @@ -27,4 +27,30 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) logger.error("Failed to retrieve logs"); return { error: "Failed to retrieve logs" }; } + }) + + .delete("/", async ({ set }) => { + try { + set.status = 200; + set.headers["Content-Type"] = "application/json"; + dbFunctions.clearAllLogs(); + return { success: true }; + } catch (error) { + set.status = 500; + logger.error("Could not delete all logs,", error); + return { error: "Could not delete all logs" }; + } + }) + + .delete("/:level", async ({ params: { level }, set }) => { + try { + dbFunctions.clearLogsByLevel(level); + set.headers["Content-Type"] = "application/json"; + logger.debug(`Cleared all logs with level: ${level}`); + return { success: true }; + } catch (error) { + set.status = 500; + logger.error("Could not clear logs with level", level, ",", error); + return { error: "Failed to retrieve logs" }; + } }); diff --git a/src/typings/docker.ts b/src/typings/docker.ts new file mode 100644 index 0000000..e5294bb --- /dev/null +++ b/src/typings/docker.ts @@ -0,0 +1,28 @@ +interface DockerHost { + name: string; + url: string; + poll_interval: number; +} + +interface ContainerInfo { + id: string; + hostId: string; + name: string; + image: string; + status: string; + state: string; + cpuUsage: number; + memoryUsage: number; +} + +interface HostConfig { + hostId: string; + dockerVersion: string; + apiVersion: string; + os: string; + architecture: string; + totalMemory: number; + totalCPU: number; +} + +export type { HostConfig, ContainerInfo, DockerHost }; From 32ffec8b1293531eca60053b03937527ece95794 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 25 Feb 2025 21:40:54 +0100 Subject: [PATCH 136/324] ToFix: Sockets are not closing after disconnecting --- bun.lock | 6 + package.json | 2 + src/core/database/repository.ts | 93 ++++---- src/core/docker/client.ts | 20 ++ src/core/docker/host-manager.ts | 38 ---- src/core/utils/calculations.ts | 16 ++ src/core/utils/logger.ts | 2 +- src/core/utils/respone-handler.ts | 46 ++++ src/index.ts | 25 ++- src/routes/api-config.ts | 52 +++++ src/routes/docker-manager.ts | 84 ++++++-- src/routes/docker-stats.ts | 345 +++++++++++------------------- src/routes/docker-websocket.ts | 201 +++++++++++++++++ src/routes/logs.ts | 128 ++++++----- src/typings/database.ts | 13 ++ src/typings/docker.ts | 2 +- 16 files changed, 701 insertions(+), 372 deletions(-) create mode 100644 src/core/docker/client.ts delete mode 100644 src/core/docker/host-manager.ts create mode 100644 src/core/utils/calculations.ts create mode 100644 src/core/utils/respone-handler.ts create mode 100644 src/routes/api-config.ts create mode 100644 src/routes/docker-websocket.ts create mode 100644 src/typings/database.ts diff --git a/bun.lock b/bun.lock index cf94952..2c9571a 100644 --- a/bun.lock +++ b/bun.lock @@ -6,9 +6,11 @@ "dependencies": { "@elysiajs/swagger": "^1.2.2", "@types/dockerode": "^3.3.34", + "@types/split2": "^4.2.3", "chalk": "^5.4.1", "dockerode": "^4.0.4", "elysia": "latest", + "split2": "^4.2.0", "winston": "^3.17.0", "winston-transport": "^4.9.0", }, @@ -69,6 +71,8 @@ "@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], + "@types/split2": ["@types/split2@4.2.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-59OXIlfUsi2k++H6CHgUQKEb2HKRokUA39HY1i1dS8/AIcqVjtAAFdf8u+HxTWK/4FUHMJQlKSZ4I6irCBJ1Zw=="], + "@types/ssh2": ["@types/ssh2@1.15.4", "", { "dependencies": { "@types/node": "^18.11.18" } }, "sha512-9JTQgVBWSgq6mAen6PVnrAmty1lqgCMvpfN+1Ck5WRUsyMYPa6qd50/vMJ0y1zkGpOEgLzm8m8Dx/Y5vRouLaA=="], "@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="], @@ -195,6 +199,8 @@ "split-ca": ["split-ca@1.0.1", "", {}, "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ=="], + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + "ssh2": ["ssh2@1.16.0", "", { "dependencies": { "asn1": "^0.2.6", "bcrypt-pbkdf": "^1.0.2" }, "optionalDependencies": { "cpu-features": "~0.0.10", "nan": "^2.20.0" } }, "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg=="], "stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="], diff --git a/package.json b/package.json index d754838..ff72d90 100644 --- a/package.json +++ b/package.json @@ -8,9 +8,11 @@ "dependencies": { "@elysiajs/swagger": "^1.2.2", "@types/dockerode": "^3.3.34", + "@types/split2": "^4.2.3", "chalk": "^5.4.1", "dockerode": "^4.0.4", "elysia": "latest", + "split2": "^4.2.0", "winston": "^3.17.0", "winston-transport": "^4.9.0" }, diff --git a/src/core/database/repository.ts b/src/core/database/repository.ts index 6ef778b..6c0a62b 100644 --- a/src/core/database/repository.ts +++ b/src/core/database/repository.ts @@ -1,6 +1,8 @@ import Database from "bun:sqlite"; import { logger } from "~/core/utils/logger"; import { typeCheck } from "~/core/utils/type-check"; +import { config } from "~/typings/database"; +import type { DockerHost } from "~/typings/docker"; const db = new Database("dockstatapi.db"); @@ -10,15 +12,11 @@ export const dbFunctions = { CREATE TABLE IF NOT EXISTS docker_hosts ( name TEXT, url TEXT, - poll_interval INTEGER + secure BOOLEAN ); - CREATE TABLE IF NOT EXISTS container_metrics ( - host_id TEXT, - container_id TEXT, - cpu REAL, - memory REAL, - timestamp DATETIME DEFAULT CURRENT_TIMESTAMP + CREATE TABLE IF NOT EXISTS config ( + polling_rate NUMBER ); CREATE TABLE IF NOT EXISTS backend_log_entries ( @@ -29,54 +27,46 @@ export const dbFunctions = { line NUMBER ); `); + + const configRow = db + .prepare(`SELECT COUNT(*) AS count FROM config`) + .get() as { count: number }; + if (configRow.count === 0) { + const stmt = db.prepare( + ` + INSERT INTO config (polling_rate) VALUES (5) + `, + ); + + stmt.run(); + } }, - addDockerHost(hostId: string, url: string, pollInterval: number) { + addDockerHost(hostId: string, url: string, secure: boolean) { if ( !typeCheck(hostId, "string") || !typeCheck(url, "string") || - !typeCheck(pollInterval, "number") + !typeCheck(secure, "boolean") ) { logger.crit("Invalid parameter types for addDockerHost"); throw new TypeError("Invalid parameter types for addDockerHost"); } const stmt = db.prepare(` - INSERT INTO docker_hosts (name, url, poll_interval) + INSERT INTO docker_hosts (name, url, secure) VALUES (?, ?, ?) `); - return stmt.run(hostId, url, pollInterval); + return stmt.run(hostId, url, secure); }, - getDockerHosts() { + getDockerHosts(): DockerHost[] { const stmt = db.prepare(` - SELECT name, url, poll_interval + SELECT name, url, secure FROM docker_hosts ORDER BY name DESC `); - return stmt.all(); - }, - - insertMetric(hostId: string, metric: any) { - if (!typeCheck(hostId, "string") || !typeCheck(metric, "object")) { - logger.crit("Invalid parameter types for insertMetric"); - throw new TypeError("Invalid parameter types for insertMetric"); - } - - if ( - !typeCheck(metric.containerId, "string") || - !typeCheck(metric.cpu, "number") || - !typeCheck(metric.memory, "number") - ) { - logger.crit("Invalid metric object structure"); - throw new TypeError("Invalid metric object structure"); - } - - const stmt = db.prepare(` - INSERT INTO container_metrics (host_id, container_id, cpu, memory) - VALUES (?, ?, ?, ?) - `); - return stmt.run(hostId, metric.containerId, metric.cpu, metric.memory); + const data = stmt.all(); + return data as DockerHost[]; }, addLogEntry: ( @@ -126,11 +116,11 @@ export const dbFunctions = { return stmt.all(level); }, - updateDockerHost(name: string, url: string, pollInterval: number) { + updateDockerHost(name: string, url: string, secure: boolean) { if ( !typeCheck(name, "string") || !typeCheck(url, "string") || - !typeCheck(pollInterval, "number") + !typeCheck(secure, "boolean") ) { logger.crit("Invalid parameter types for updateDockerHost"); throw new TypeError("Invalid parameter types for updateDockerHost"); @@ -138,10 +128,10 @@ export const dbFunctions = { const stmt = db.prepare(` UPDATE docker_hosts - SET url = ?, poll_interval = ? + SET url = ?, secure = ? WHERE name = ? `); - return stmt.run(url, pollInterval, name); + return stmt.run(url, secure, name); }, deleteDockerHost(name: string) { @@ -176,6 +166,29 @@ export const dbFunctions = { `); return stmt.run(level); }, + + updateConfig(polling_rate: number) { + if (!typeCheck(polling_rate, "number")) { + logger.crit("Invalid parameter type for updateConfig"); + throw new TypeError("Polling rate must be a number!"); + } + + const stmt = db.prepare(` + UPDATE config + SET polling_rate = ? + `); + + return stmt.run(polling_rate); + }, + + getConfig() { + const stmt = db.prepare(` + SELECT distinct(polling_rate) + FROM config + `); + + return stmt.all(); + }, }; dbFunctions.init(); diff --git a/src/core/docker/client.ts b/src/core/docker/client.ts new file mode 100644 index 0000000..da17403 --- /dev/null +++ b/src/core/docker/client.ts @@ -0,0 +1,20 @@ +import type { DockerHost } from "~/typings/docker"; +import Docker from "dockerode"; +import { logger } from "~/core/utils/logger"; + +export const getDockerClient = (host: DockerHost): Docker => { + try { + const [hostAddress, port] = host.url.split(":"); + const protocol = host.secure ? "https" : "http"; + return new Docker({ + protocol, + host: hostAddress, + port: port ? parseInt(port) : host.secure ? 2376 : 2375, + version: "v1.41", + // TODO: Add TLS configuration if needed + }); + } catch (error) { + logger.error("Invalid Docker host URL configuration,", error); + throw new Error("Invalid Docker host configuration"); + } +}; diff --git a/src/core/docker/host-manager.ts b/src/core/docker/host-manager.ts deleted file mode 100644 index e2c1ccc..0000000 --- a/src/core/docker/host-manager.ts +++ /dev/null @@ -1,38 +0,0 @@ -import WebSocket from "ws"; -import { pluginManager } from "~/core/plugins/plugin-manager"; -import { dbFunctions } from "~/core/database/repository"; -import { logger } from "~/core/utils/logger"; - -export class DockerHostManager { - public connections = new Map(); - - async connect(hostId: string, url: string) { - const ws = new WebSocket(url); - - ws.on("open", () => { - this.connections.set(hostId, ws); - logger.info(`Opened connection to ${hostId}`); - }); - - ws.on("message", (data) => { - this.handleData(hostId, JSON.parse(data.toString())); - }); - - ws.on("close", () => { - this.connections.delete(hostId); - logger.info(`Disconnected from Docker host ${hostId}`); - }); - } - - private handleData(hostId: string, data: any) { - dbFunctions.insertMetric(hostId, data); - - if (data.event === "container_start") { - pluginManager.handleContainerStart(data.container); - } - - pluginManager.handleMetrics(data); - } -} - -export const dockerHostManager = new DockerHostManager(); diff --git a/src/core/utils/calculations.ts b/src/core/utils/calculations.ts new file mode 100644 index 0000000..3ead3a6 --- /dev/null +++ b/src/core/utils/calculations.ts @@ -0,0 +1,16 @@ +import type Docker from "dockerode"; + +const calculateCpuPercent = (stats: Docker.ContainerStats): number => { + const cpuDelta = + stats.cpu_stats.cpu_usage.total_usage - + stats.precpu_stats.cpu_usage.total_usage; + const systemDelta = + stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage; + return (cpuDelta / systemDelta) * 100; +}; + +const calculateMemoryUsage = (stats: Docker.ContainerStats): number => { + return (stats.memory_stats.usage / stats.memory_stats.limit) * 100; +}; + +export { calculateCpuPercent, calculateMemoryUsage }; diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts index 1675d23..c704d0a 100644 --- a/src/core/utils/logger.ts +++ b/src/core/utils/logger.ts @@ -1,7 +1,7 @@ import { createLogger, format, transports } from "winston"; import Transport from "winston-transport"; import path from "path"; -import { dbFunctions } from "../database/repository"; +import { dbFunctions } from "~/core/database/repository"; import chalk, { ChalkInstance } from "chalk"; const fileLineFormat = format((info) => { diff --git a/src/core/utils/respone-handler.ts b/src/core/utils/respone-handler.ts new file mode 100644 index 0000000..93e0cdb --- /dev/null +++ b/src/core/utils/respone-handler.ts @@ -0,0 +1,46 @@ +import { logger } from "~/core/utils/logger"; +import type { HTTPHeaders } from "elysia/dist/types"; +import type { ElysiaCookie } from "elysia/dist/cookies"; +import type { StatusMap } from "elysia"; + +interface set { + headers: HTTPHeaders; + status?: number | keyof StatusMap; + redirect?: string; + cookie?: Record; +} + +export const responseHandler = { + error( + set: set, + error: string, + response_message: string, + error_code?: number, + ) { + set.status = error_code || 500; + logger.error(`${response_message} - ${error}`); + return { error: `${response_message}` }; + }, + + ok(set: set, response_message: string) { + set.status = 200; + logger.debug(response_message); + return { success: true }; + }, + + simple_error(set: set, response_massage: string, status_code?: number) { + set.status = status_code || 502; + logger.warn(response_massage); + return { error: response_massage }; + }, + + reject(set: set, reject: any, response_message: string, error?: string) { + set.status = 501; + if (error) { + logger.error(`${response_message} - ${error}`); + } else { + logger.error(response_message); + } + return reject(new Error(response_message)); + }, +}; diff --git a/src/index.ts b/src/index.ts index 576d42a..06e500d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,9 @@ import { loadPlugins } from "~/core/plugins/loader"; import { logger } from "~/core/utils/logger"; import { dockerRoutes } from "~/routes/docker-manager"; import { dockerStatsRoutes } from "~/routes/docker-stats"; -import { backendLogs } from "./routes/logs"; +import { backendLogs } from "~/routes/logs"; +import { dockerWebsocketRoutes } from "~/routes/docker-websocket"; +import { apiConfigRoutes } from "~/routes/api-config"; dbFunctions.init(); @@ -18,20 +20,37 @@ const app = new Elysia() version: "2.1.0", description: "Docker monitoring API with plugin support", }, + tags: [ + { + name: "Statistics", + description: + "All endpoints for fetching statistics of hosts / containers", + }, + { + name: "Management", + description: "Various endpoints for managing DockStatAPI", + }, + { + name: "Utils", + description: "Various utilities which might be useful", + }, + ], }, }), ) .use(dockerRoutes) .use(dockerStatsRoutes) .use(backendLogs) - .get("/health", () => ({ status: "healthy" })); + .use(dockerWebsocketRoutes) + .use(apiConfigRoutes) + .get("/health", () => ({ status: "healthy" }), { tags: ["Utils"] }); async function startServer() { try { await loadPlugins("./plugins"); app.listen(3000, ({ hostname, port }) => { - logger.info(`DockStat is running at http://${hostname}:${port}`); + logger.info(`DockStatAPI is running at http://${hostname}:${port}`); logger.info( `Swagger API Documentation available at http://${hostname}:${port}/swagger`, ); diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts new file mode 100644 index 0000000..41262c8 --- /dev/null +++ b/src/routes/api-config.ts @@ -0,0 +1,52 @@ +import { Elysia, t } from "elysia"; +import { dbFunctions } from "~/core/database/repository"; +import { logger } from "~/core/utils/logger"; +import { responseHandler } from "~/core/utils/respone-handler"; +import { config } from "~/typings/database"; + +export const apiConfigRoutes = new Elysia({ prefix: "/config" }) + .get( + "/get", + async ({ set }) => { + try { + const data = dbFunctions.getConfig() as config[]; + const distinct = data[0]; + set.status = 200; + set.headers["Content-Type"] = "application/json"; + logger.debug("Fetched backend config"); + return distinct; + } catch (error) { + return responseHandler.error( + set, + "Error getting the DockStatAPI config", + error as string, + ); + } + }, + { + tags: ["Management"], + }, + ) + .post( + "/update", + async ({ set, body }) => { + try { + const { polling_rate } = body; + set.headers["Content-Type"] = "application/json"; + dbFunctions.updateConfig(polling_rate); + return responseHandler.ok(set, "Updated DockStatAPI config"); + } catch (error) { + return responseHandler.error( + set, + "Error updating the DockStatAPI config", + error as string, + ); + } + }, + { + body: t.Object({ + polling_rate: t.Number(), + }), + tags: ["Management"], + }, + ); diff --git a/src/routes/docker-manager.ts b/src/routes/docker-manager.ts index ed2e5ff..eb53fdb 100644 --- a/src/routes/docker-manager.ts +++ b/src/routes/docker-manager.ts @@ -1,42 +1,82 @@ import { Elysia, t } from "elysia"; -import { dockerHostManager } from "~/core/docker/host-manager"; import { dbFunctions } from "~/core/database/repository"; import { logger } from "~/core/utils/logger"; +import { responseHandler } from "~/core/utils/respone-handler"; export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) .post( "/add-host", async ({ set, body }) => { try { - const { id, url, pollInterval } = body; + const { name, url, secure } = body; set.headers["Content-Type"] = "application/json"; - dbFunctions.addDockerHost(id, url, pollInterval); - logger.debug(`Added docker host (${id})`); - return { success: true }; + dbFunctions.addDockerHost(name, url, secure); + return responseHandler.ok(set, `Added docker host (${name})`); + } catch (error: unknown) { + return responseHandler.error( + set, + "Error adding docker Host", + error as string, + ); + } + }, + { + detail: { + tags: ["Management"], + }, + body: t.Object({ + name: t.String(), + url: t.String(), + secure: t.Boolean(), + }), + }, + ) + + .post( + "/update-host", + async ({ set, body }) => { + try { + const { name, url, secure } = body; + dbFunctions.updateDockerHost(name, url, secure); } catch (error) { - set.status = 500; - logger.error("Failed to add host,", error); - return { error: "Failed to add host" }; + return responseHandler.error( + set, + error as string, + "Failed to update host", + ); } }, { + detail: { + tags: ["Management"], + }, body: t.Object({ - id: t.String(), + name: t.String(), url: t.String(), - pollInterval: t.Number(), + secure: t.Boolean(), }), }, ) - .get("/hosts", async ({ set }) => { - try { - const dockerHosts = dbFunctions.getDockerHosts(); - set.headers["Content-Type"] = "application/json"; - logger.debug("Retrieved docker hosts"); - return dockerHosts; - } catch (error) { - set.status = 500; - logger.error("Failed to retrieve hosts,", error); - return { error: "Failed to retrieve hosts" }; - } - }); + .get( + "/hosts", + async ({ set }) => { + try { + const dockerHosts = dbFunctions.getDockerHosts(); + set.headers["Content-Type"] = "application/json"; + logger.debug("Retrieved docker hosts"); + return dockerHosts; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to retrieve hosts", + ); + } + }, + { + detail: { + tags: ["Management"], + }, + }, + ); diff --git a/src/routes/docker-stats.ts b/src/routes/docker-stats.ts index 5bb9da2..95e4a8b 100644 --- a/src/routes/docker-stats.ts +++ b/src/routes/docker-stats.ts @@ -1,243 +1,150 @@ -import { Elysia, t } from "elysia"; import Docker from "dockerode"; +import { Elysia } from "elysia"; import { dbFunctions } from "~/core/database/repository"; +import { getDockerClient } from "~/core/docker/client"; +import { + calculateCpuPercent, + calculateMemoryUsage, +} from "~/core/utils/calculations"; import { logger } from "~/core/utils/logger"; -import type { HostConfig, DockerHost, ContainerInfo } from "~/typings/docker"; - -interface WsData { - params: any; - interval?: ReturnType; - statsStream?: any; -} - -const getDockerClient = (hostUrl: string): Docker => { - try { - const [host, port] = hostUrl.includes("://") - ? hostUrl.split("://")[1].split(":") - : hostUrl.split(":"); - - const protocol = hostUrl.startsWith("https://") ? "https" : "http"; - - return new Docker({ - protocol, - host, - port: port ? parseInt(port) : protocol === "https" ? 2376 : 2375, - version: "v1.41", - // TODO: Add TLS configuration if needed - }); - } catch (error) { - logger.error("Invalid Docker host URL configuration,", error); - throw new Error("Invalid Docker host configuration"); - } -}; +import { responseHandler } from "~/core/utils/respone-handler"; +import type { ContainerInfo, DockerHost, HostConfig } from "~/typings/docker"; export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) - .get("/containers", async ({ set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const containers: ContainerInfo[] = []; - - await Promise.all( - hosts.map(async (host) => { - try { - const docker = getDockerClient(host.url); - try { - await docker.ping(); - } catch (pingError) { - logger.error("Docker host connection failed,", pingError); - return; - } - - const hostContainers = await docker.listContainers({ all: true }); - - await Promise.all( - hostContainers.map(async (containerInfo) => { - try { - const container = docker.getContainer(containerInfo.Id); - const stats = await new Promise( - (resolve, reject) => { - container.stats({ stream: false }, (err, stats) => { - if (err) { - logger.error("An error occured,", err); - return reject(err); - } - if (!stats) { - logger.error("No stats available"); - return reject(new Error("No stats available")); - } - resolve(stats); - }); - }, - ); - - containers.push({ - id: containerInfo.Id, - hostId: host.name, - name: containerInfo.Names[0].replace(/^\//, ""), - image: containerInfo.Image, - status: containerInfo.Status, - state: containerInfo.State, - cpuUsage: calculateCpuPercent(stats), - memoryUsage: calculateMemoryUsage(stats), - }); - } catch (containerError) { - logger.error( - "Error fetching container stats,", - containerError, - ); - } - }), - ); - logger.debug(`Fetched stats for ${host.name}`); - } catch (hostError) { - logger.error("Error fetching containers for host,", hostError); - } - }), - ); - - set.headers["Content-Type"] = "application/json"; - return { containers }; - } catch (error) { - set.status = 500; - logger.error("Failed to retrieve containers,", error); - return { error: "Failed to retrieve containers" }; - } - }) - - .get("/hosts/:id/config", async ({ params, set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const host = hosts.find((h) => h.name === params.id); - - if (!host) { - set.status = 404; - logger.error(`Host (${host}) not found`); - return { error: "Host not found" }; - } - - const docker = getDockerClient(host.url); - const info = await docker.info(); - - const config: HostConfig = { - hostId: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - }; - - set.headers["Content-Type"] = "application/json"; - logger.debug(`Fetched config for ${host.name}`); - return config; - } catch (error) { - set.status = 500; - logger.error("Failed to retrieve host config,", error); - return { error: "Failed to retrieve host config" }; - } - }) - - .ws("/hosts/:id/stats", { - message(ws, message) { - ws.send(message); - }, - async open(ws) { + .get( + "/containers", + async ({ set }) => { try { const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const host = hosts.find((h) => h.name === ws.data.params.id); + const containers: ContainerInfo[] = []; - if (!host) { - ws.close(1008, "Host not found"); - logger.error(`Host (${host}) not found`); - return; - } + await Promise.all( + hosts.map(async (host) => { + try { + const docker = getDockerClient(host); + try { + await docker.ping(); + } catch (pingError) { + return responseHandler.error( + set, + pingError as string, + "Docker host connection failed", + ); + } + + const hostContainers = await docker.listContainers({ all: true }); + + await Promise.all( + hostContainers.map(async (containerInfo) => { + try { + const container = docker.getContainer(containerInfo.Id); + const stats = await new Promise( + (resolve, reject) => { + container.stats({ stream: false }, (error, stats) => { + if (error) { + return responseHandler.reject( + set, + reject, + "An error occurred", + error, + ); + } + if (!stats) { + return responseHandler.reject( + set, + reject, + "No stats available", + ); + } + resolve(stats); + }); + }, + ); + + containers.push({ + id: containerInfo.Id, + hostId: host.name, + name: containerInfo.Names[0].replace(/^\//, ""), + image: containerInfo.Image, + status: containerInfo.Status, + state: containerInfo.State, + cpuUsage: calculateCpuPercent(stats), + memoryUsage: calculateMemoryUsage(stats), + }); + } catch (containerError) { + logger.error( + "Error fetching container stats,", + containerError, + ); + } + }), + ); + logger.debug(`Fetched stats for ${host.name}`); + } catch (hostError) { + logger.error("Error fetching containers for host,", hostError); + } + }), + ); - const docker = getDockerClient(host.url); - const interval = setInterval(async () => { - try { - const info = await docker.info(); - ws.send({ - timestamp: Date.now(), - memoryUsage: info.MemTotal - info.MemFree, - cpuUsage: info.NanoCPUs, - containerCount: info.ContainersRunning, - }); - logger.debug(`Fetched host (${host.name}) config`); - } catch (error) { - logger.error("Error fetching host stats,", error); - } - }, 5000); - (ws.data as WsData).interval = interval; + set.headers["Content-Type"] = "application/json"; + logger.debug("Fetched all containers across all hosts"); + return { containers }; } catch (error) { - logger.error("WebSocket connection failed,", error); - ws.close(1011, "Internal error"); + return responseHandler.error( + set, + error as string, + "Failed to retrieve containers", + ); } }, - close(ws) { - const data = ws.data as WsData; - if (data.interval) { - clearInterval(data.interval); - } + { + detail: { + tags: ["Statistics"], + }, }, - }) + ) - .ws("/containers/:hostId/:containerId/metrics", { - message(ws, message) { - ws.send(message); - }, - async open(ws) { + .get( + "/hosts/:id", + async ({ params, set }) => { try { const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const host = hosts.find((h) => h.name === ws.data.params.hostId); + const host = hosts.find((h) => h.name === params.id); if (!host) { - ws.close(1008, "Host not found"); - logger.error(`Host (${host}) not found`); - return; + return responseHandler.simple_error( + set, + `Host (${params.id}) not found`, + ); } - const docker = getDockerClient(host.url); - const container = docker.getContainer(ws.data.params.containerId); - const statsStream = await container.stats({ stream: true }); - - statsStream.on("data", (data: Buffer) => { - const stats = JSON.parse(data.toString()); - ws.send({ - cpu: calculateCpuPercent(stats), - memory: calculateMemoryUsage(stats), - timestamp: Date.now(), - }); - }); - - statsStream.on("error", (error) => { - logger.error("Container stats stream error,", error); - ws.close(1011, "Stats stream error"); - }); - - (ws.data as WsData).statsStream = statsStream; + const docker = getDockerClient(host); + const info = await docker.info(); + + const config: HostConfig = { + hostId: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + }; + + set.headers["Content-Type"] = "application/json"; + logger.debug(`Fetched config for ${host.name}`); + return config; } catch (error) { - logger.error("WebSocket connection failed,", error); - ws.close(1011, "Internal error"); + return responseHandler.error( + set, + error as string, + "Failed to retrieve host config", + ); } }, - close(ws) { - const data = ws.data as WsData; - if (data.statsStream) { - data.statsStream.destroy(); - } + { + detail: { + tags: ["Statistics"], + }, }, - }); - -const calculateCpuPercent = (stats: Docker.ContainerStats): number => { - const cpuDelta = - stats.cpu_stats.cpu_usage.total_usage - - stats.precpu_stats.cpu_usage.total_usage; - const systemDelta = - stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage; - return (cpuDelta / systemDelta) * 100; -}; - -const calculateMemoryUsage = (stats: Docker.ContainerStats): number => { - return (stats.memory_stats.usage / stats.memory_stats.limit) * 100; -}; + ); diff --git a/src/routes/docker-websocket.ts b/src/routes/docker-websocket.ts new file mode 100644 index 0000000..ef7b720 --- /dev/null +++ b/src/routes/docker-websocket.ts @@ -0,0 +1,201 @@ +import type { StatusMap } from "elysia"; +import { Elysia } from "elysia"; +import type { HTTPHeaders } from "elysia/dist/types"; +import { dbFunctions } from "~/core/database/repository"; +import { getDockerClient } from "~/core/docker/client"; +import { + calculateCpuPercent, + calculateMemoryUsage, +} from "~/core/utils/calculations"; +import { logger } from "~/core/utils/logger"; +import { responseHandler } from "~/core/utils/respone-handler"; +import type { DockerHost } from "~/typings/docker"; +import split2 from "split2"; + +const set: { headers: HTTPHeaders; status?: number | keyof StatusMap } = { + headers: {}, +}; + +export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( + "/stats", + { + async open(socket) { + socket.send(JSON.stringify({ message: "Connection established" })); + let hosts: DockerHost[]; + + // Track if the WebSocket is open + (socket as any).isOpen = true; + (socket as any).streams = []; + + logger.debug(`Opened WebSocket (${socket.id})`); + + try { + hosts = dbFunctions.getDockerHosts(); + logger.debug( + `Retrieved ${hosts.length} docker host(s) from the database`, + ); + } catch (error: unknown) { + const errResponse = responseHandler.error( + set, + (error as Error).message, + "Failed to retrieve Docker hosts", + 500, + ); + logger.error( + `Error retrieving Docker hosts: ${(error as Error).message}`, + ); + socket.send(JSON.stringify(errResponse)); + return; + } + + for (const host of hosts) { + if (!(socket as any).isOpen) break; + + logger.debug(`Processing host: ${host.name}`); + + try { + const docker = getDockerClient(host); + await docker.ping(); + logger.debug(`Ping successful for host: ${host.name}`); + logger.debug(`Listing containers for host: ${host.name}`); + const containers = await docker.listContainers(); + logger.debug( + `Found ${containers.length} container(s) on host ${host.name}`, + ); + + for (const containerInfo of containers) { + // Check if WebSocket is still open before processing each container + if (!(socket as any).isOpen) break; + + logger.debug( + `Processing container ${containerInfo.Id} on host ${host.name}`, + ); + const container = docker.getContainer(containerInfo.Id); + try { + logger.debug( + `Starting stats stream for container ${containerInfo.Id} on host ${host.name}`, + ); + const statsStream = await container.stats({ stream: true }); + + // Immediately destroy stream if WebSocket closed while setting up + if (!(socket as any).isOpen) { + statsStream.pause(); + statsStream.unpipe(); + continue; + } + + // Save stream for cleanup on socket close + (socket as any).streams.push(statsStream); + + // Use split2 to process NDJSON lines + statsStream + .pipe(split2()) + .on("data", (line: string) => { + if (!line) return; + try { + const stats = JSON.parse(line); + const cpuUsage = calculateCpuPercent(stats); + const memoryUsage = calculateMemoryUsage(stats); + + const data = { + id: containerInfo.Id, + hostId: host.name, + name: containerInfo.Names[0].replace(/^\//, ""), + image: containerInfo.Image, + status: containerInfo.Status, + state: containerInfo.State, + cpuUsage, + memoryUsage, + }; + socket.send(JSON.stringify(data)); + logger.debug(`Parsing data`); + } catch (parseErr: any) { + logger.error( + `Failed to parse stats for container ${containerInfo.Id} on host ${host.name}: ${parseErr.message}`, + ); + } + }) + .on("error", (err: Error) => { + logger.error( + `Stats stream error for container ${containerInfo.Id} on host ${host.name}: ${err.message}`, + ); + const errResponse = responseHandler.error( + set, + err.message, + `Stats stream error for container ${containerInfo.Id}`, + 500, + ); + socket.send( + JSON.stringify({ + hostId: host.name, + containerId: containerInfo.Id, + error: errResponse.error, + }), + ); + statsStream.removeAllListeners(); + }); + + statsStream.resume(); + } catch (streamErr: any) { + logger.error( + `Failed to start stats stream for container ${containerInfo.Id} on host ${host.name}: ${streamErr.message}`, + ); + const errResponse = responseHandler.error( + set, + streamErr.message, + `Failed to start stats stream for container ${containerInfo.Id}`, + 500, + ); + socket.send( + JSON.stringify({ + hostId: host.name, + containerId: containerInfo.Id, + error: errResponse.error, + }), + ); + } + } + } catch (err: any) { + logger.error( + `Failed to list containers for host ${host.name}: ${err.message}`, + ); + const errResponse = responseHandler.error( + set, + err.message, + `Failed to list containers for host ${host.name}`, + 500, + ); + socket.send( + JSON.stringify({ + hostId: host.name, + error: errResponse.error, + }), + ); + } + } + }, + + close(socket, code, reason) { + //socket.isOpen = false; + + socket.close(1000); + //const streams = (socket as any).streams; + //if (streams?.length) { + // streams.forEach((stream: NodeJS.ReadableStream) => { + // try { + // logger.debug(`Destroying stats stream`); + // stream.pause(); + // stream.unpipe(); + // } catch (err) { + // logger.error(`Error destroying stream: ${err}`); + // } + // }); + // (socket as any).streams = []; + //} + + logger.info( + `Closed WebSocket (${socket.id}) - Code: ${code} - Reason: ${reason}`, + ); + }, + }, +); diff --git a/src/routes/logs.ts b/src/routes/logs.ts index 5501f09..a8cae1c 100644 --- a/src/routes/logs.ts +++ b/src/routes/logs.ts @@ -3,54 +3,86 @@ import { dbFunctions } from "~/core/database/repository"; import { logger } from "~/core/utils/logger"; export const backendLogs = new Elysia({ prefix: "/logs" }) - .get("/", async ({ set }) => { - try { - const logs = dbFunctions.getAllLogs(); - set.headers["Content-Type"] = "application/json"; - logger.debug(`Retrieved all logs`); - return logs; - } catch (error) { - set.status = 500; - logger.error("Failed to retrieve logs,", error); - return { error: "Failed to retrieve logs" }; - } - }) + .get( + "/", + async ({ set }) => { + try { + const logs = dbFunctions.getAllLogs(); + set.headers["Content-Type"] = "application/json"; + logger.debug(`Retrieved all logs`); + return logs; + } catch (error) { + set.status = 500; + logger.error("Failed to retrieve logs,", error); + return { error: "Failed to retrieve logs" }; + } + }, + { + detail: { + tags: ["Management"], + }, + }, + ) - .get("/:level", async ({ params: { level }, set }) => { - try { - const logs = dbFunctions.getLogsByLevel(level); - set.headers["Content-Type"] = "application/json"; - logger.debug(`Retrieved logs (level: ${level})`); - return logs; - } catch (error) { - set.status = 500; - logger.error("Failed to retrieve logs"); - return { error: "Failed to retrieve logs" }; - } - }) + .get( + "/:level", + async ({ params: { level }, set }) => { + try { + const logs = dbFunctions.getLogsByLevel(level); + set.headers["Content-Type"] = "application/json"; + logger.debug(`Retrieved logs (level: ${level})`); + return logs; + } catch (error) { + set.status = 500; + logger.error("Failed to retrieve logs"); + return { error: "Failed to retrieve logs" }; + } + }, + { + detail: { + tags: ["Management"], + }, + }, + ) - .delete("/", async ({ set }) => { - try { - set.status = 200; - set.headers["Content-Type"] = "application/json"; - dbFunctions.clearAllLogs(); - return { success: true }; - } catch (error) { - set.status = 500; - logger.error("Could not delete all logs,", error); - return { error: "Could not delete all logs" }; - } - }) + .delete( + "/", + async ({ set }) => { + try { + set.status = 200; + set.headers["Content-Type"] = "application/json"; + dbFunctions.clearAllLogs(); + return { success: true }; + } catch (error) { + set.status = 500; + logger.error("Could not delete all logs,", error); + return { error: "Could not delete all logs" }; + } + }, + { + detail: { + tags: ["Management"], + }, + }, + ) - .delete("/:level", async ({ params: { level }, set }) => { - try { - dbFunctions.clearLogsByLevel(level); - set.headers["Content-Type"] = "application/json"; - logger.debug(`Cleared all logs with level: ${level}`); - return { success: true }; - } catch (error) { - set.status = 500; - logger.error("Could not clear logs with level", level, ",", error); - return { error: "Failed to retrieve logs" }; - } - }); + .delete( + "/:level", + async ({ params: { level }, set }) => { + try { + dbFunctions.clearLogsByLevel(level); + set.headers["Content-Type"] = "application/json"; + logger.debug(`Cleared all logs with level: ${level}`); + return { success: true }; + } catch (error) { + set.status = 500; + logger.error("Could not clear logs with level", level, ",", error); + return { error: "Failed to retrieve logs" }; + } + }, + { + detail: { + tags: ["Management"], + }, + }, + ); diff --git a/src/typings/database.ts b/src/typings/database.ts new file mode 100644 index 0000000..d39ccbf --- /dev/null +++ b/src/typings/database.ts @@ -0,0 +1,13 @@ +interface backend_log_entries { + timestamp: string; + level: string; + message: string; + file: string; + line: number; +} + +interface config { + polling_rate: number; +} + +export type { backend_log_entries, config }; diff --git a/src/typings/docker.ts b/src/typings/docker.ts index e5294bb..8d78ae2 100644 --- a/src/typings/docker.ts +++ b/src/typings/docker.ts @@ -1,7 +1,7 @@ interface DockerHost { name: string; url: string; - poll_interval: number; + secure: boolean; } interface ContainerInfo { From fef8744603a327b10419d5d92feb6800199cc19b Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 25 Feb 2025 22:36:16 +0100 Subject: [PATCH 137/324] Fix: Webocket cleanup works? Let's go? --- src/routes/docker-websocket.ts | 148 ++++++++++++++++++++------------- 1 file changed, 92 insertions(+), 56 deletions(-) diff --git a/src/routes/docker-websocket.ts b/src/routes/docker-websocket.ts index ef7b720..09770dc 100644 --- a/src/routes/docker-websocket.ts +++ b/src/routes/docker-websocket.ts @@ -11,6 +11,7 @@ import { logger } from "~/core/utils/logger"; import { responseHandler } from "~/core/utils/respone-handler"; import type { DockerHost } from "~/typings/docker"; import split2 from "split2"; +import type { Readable } from "stream"; const set: { headers: HTTPHeaders; status?: number | keyof StatusMap } = { headers: {}, @@ -26,6 +27,7 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( // Track if the WebSocket is open (socket as any).isOpen = true; (socket as any).streams = []; + (socket as any).heartbeat = null; // Add heartbeat reference logger.debug(`Opened WebSocket (${socket.id})`); @@ -48,6 +50,15 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( return; } + // Add heartbeat using WebSocket protocol-level ping + (socket as any).heartbeat = setInterval(() => { + if (!(socket as any).isOpen) { + clearInterval((socket as any).heartbeat); + return; + } + socket.ping(); // Use WebSocket protocol ping + }, 30000); + for (const host of hosts) { if (!(socket as any).isOpen) break; @@ -64,7 +75,6 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( ); for (const containerInfo of containers) { - // Check if WebSocket is still open before processing each container if (!(socket as any).isOpen) break; logger.debug( @@ -75,22 +85,30 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( logger.debug( `Starting stats stream for container ${containerInfo.Id} on host ${host.name}`, ); - const statsStream = await container.stats({ stream: true }); + const statsStream = (await container.stats({ + stream: true, + })) as Readable; + const splitStream = split2(); - // Immediately destroy stream if WebSocket closed while setting up - if (!(socket as any).isOpen) { - statsStream.pause(); - statsStream.unpipe(); - continue; - } + // Store both streams for cleanup + (socket as any).streams.push({ statsStream, splitStream }); - // Save stream for cleanup on socket close - (socket as any).streams.push(statsStream); + // Handle stream lifecycle + statsStream + .on("close", () => { + logger.debug(`Stats stream closed for ${containerInfo.Id}`); + splitStream.destroy(); + }) + .on("end", () => { + logger.debug(`Stats stream ended for ${containerInfo.Id}`); + splitStream.destroy(); + }); - // Use split2 to process NDJSON lines + // Process data statsStream - .pipe(split2()) + .pipe(splitStream) .on("data", (line: string) => { + if (socket.readyState !== 1) return; // 1 = OPEN state if (!line) return; try { const stats = JSON.parse(line); @@ -108,7 +126,7 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( memoryUsage, }; socket.send(JSON.stringify(data)); - logger.debug(`Parsing data`); + logger.debug(`Parsing data on Socket ${socket.id}`); } catch (parseErr: any) { logger.error( `Failed to parse stats for container ${containerInfo.Id} on host ${host.name}: ${parseErr.message}`, @@ -125,17 +143,17 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( `Stats stream error for container ${containerInfo.Id}`, 500, ); - socket.send( - JSON.stringify({ - hostId: host.name, - containerId: containerInfo.Id, - error: errResponse.error, - }), - ); - statsStream.removeAllListeners(); + if (socket.readyState === 1) { + socket.send( + JSON.stringify({ + hostId: host.name, + containerId: containerInfo.Id, + error: errResponse.error, + }), + ); + } + statsStream.destroy(); }); - - statsStream.resume(); } catch (streamErr: any) { logger.error( `Failed to start stats stream for container ${containerInfo.Id} on host ${host.name}: ${streamErr.message}`, @@ -146,13 +164,15 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( `Failed to start stats stream for container ${containerInfo.Id}`, 500, ); - socket.send( - JSON.stringify({ - hostId: host.name, - containerId: containerInfo.Id, - error: errResponse.error, - }), - ); + if (socket.readyState === 1) { + socket.send( + JSON.stringify({ + hostId: host.name, + containerId: containerInfo.Id, + error: errResponse.error, + }), + ); + } } } } catch (err: any) { @@ -165,37 +185,53 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( `Failed to list containers for host ${host.name}`, 500, ); - socket.send( - JSON.stringify({ - hostId: host.name, - error: errResponse.error, - }), - ); + if (socket.readyState === 1) { + socket.send( + JSON.stringify({ + hostId: host.name, + error: errResponse.error, + }), + ); + } } } }, + message(socket, message) { + // Handle pong responses + if (message === "pong") return; + }, + close(socket, code, reason) { - //socket.isOpen = false; - - socket.close(1000); - //const streams = (socket as any).streams; - //if (streams?.length) { - // streams.forEach((stream: NodeJS.ReadableStream) => { - // try { - // logger.debug(`Destroying stats stream`); - // stream.pause(); - // stream.unpipe(); - // } catch (err) { - // logger.error(`Error destroying stream: ${err}`); - // } - // }); - // (socket as any).streams = []; - //} - - logger.info( - `Closed WebSocket (${socket.id}) - Code: ${code} - Reason: ${reason}`, - ); + // Atomic closure flag + const wasOpen = (socket as any).isOpen; + (socket as any).isOpen = false; + + // Immediate heartbeat cleanup + clearInterval((socket as any).heartbeat); + + // Force-close streams using destructor pattern + const streams = (socket as any).streams || []; + streams.forEach(({ statsStream, splitStream }) => { + try { + // Immediate pipeline breakdown + statsStream.unpipe(splitStream); + statsStream.destroy(new Error("WebSocket closed")); + splitStream.destroy(new Error("WebSocket closed")); + + // Remove all potential listeners + statsStream.removeAllListeners(); + splitStream.removeAllListeners(); + } catch (err) { + logger.error(`Stream cleanup error: ${err}`); + } + }); + + if (wasOpen) { + logger.info( + `Closed WebSocket (${socket.id}) - Code: ${code} - Reason: ${reason}`, + ); + } }, }, ); From b1aaefbf7ad228effd80304a913ab96de72fe56b Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 26 Feb 2025 18:06:31 +0100 Subject: [PATCH 138/324] Fix: CLosing now works --- src/routes/docker-websocket.ts | 26 +++++++------------------- src/typings/websocket.ts | 9 +++++++++ 2 files changed, 16 insertions(+), 19 deletions(-) create mode 100644 src/typings/websocket.ts diff --git a/src/routes/docker-websocket.ts b/src/routes/docker-websocket.ts index 09770dc..06ea1be 100644 --- a/src/routes/docker-websocket.ts +++ b/src/routes/docker-websocket.ts @@ -12,6 +12,8 @@ import { responseHandler } from "~/core/utils/respone-handler"; import type { DockerHost } from "~/typings/docker"; import split2 from "split2"; import type { Readable } from "stream"; +import type internal from "stream"; +import type { streams } from "~/typings/websocket"; const set: { headers: HTTPHeaders; status?: number | keyof StatusMap } = { headers: {}, @@ -104,7 +106,6 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( splitStream.destroy(); }); - // Process data statsStream .pipe(splitStream) .on("data", (line: string) => { @@ -126,7 +127,6 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( memoryUsage, }; socket.send(JSON.stringify(data)); - logger.debug(`Parsing data on Socket ${socket.id}`); } catch (parseErr: any) { logger.error( `Failed to parse stats for container ${containerInfo.Id} on host ${host.name}: ${parseErr.message}`, @@ -137,39 +137,28 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( logger.error( `Stats stream error for container ${containerInfo.Id} on host ${host.name}: ${err.message}`, ); - const errResponse = responseHandler.error( - set, - err.message, - `Stats stream error for container ${containerInfo.Id}`, - 500, - ); if (socket.readyState === 1) { socket.send( JSON.stringify({ hostId: host.name, containerId: containerInfo.Id, - error: errResponse.error, + error: `Stats stream error for container ${containerInfo.Id} on host ${host.name}`, }), ); } statsStream.destroy(); }); } catch (streamErr: any) { + const errMsg = `Failed to start stats stream for container ${containerInfo.Id}`; logger.error( `Failed to start stats stream for container ${containerInfo.Id} on host ${host.name}: ${streamErr.message}`, ); - const errResponse = responseHandler.error( - set, - streamErr.message, - `Failed to start stats stream for container ${containerInfo.Id}`, - 500, - ); if (socket.readyState === 1) { socket.send( JSON.stringify({ hostId: host.name, containerId: containerInfo.Id, - error: errResponse.error, + error: errMsg, }), ); } @@ -198,12 +187,11 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( }, message(socket, message) { - // Handle pong responses if (message === "pong") return; }, close(socket, code, reason) { - // Atomic closure flag + logger.info(`Closing SplitStream and WebSocket (${socket.id})`); const wasOpen = (socket as any).isOpen; (socket as any).isOpen = false; @@ -211,7 +199,7 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( clearInterval((socket as any).heartbeat); // Force-close streams using destructor pattern - const streams = (socket as any).streams || []; + const streams: streams[] = (socket as any).streams || []; streams.forEach(({ statsStream, splitStream }) => { try { // Immediate pipeline breakdown diff --git a/src/typings/websocket.ts b/src/typings/websocket.ts new file mode 100644 index 0000000..a971247 --- /dev/null +++ b/src/typings/websocket.ts @@ -0,0 +1,9 @@ +import type { Readable } from "stream"; +import type internal from "stream"; + +interface streams { + statsStream: Readable; + splitStream: internal.Transform; +} + +export { streams }; From 90b829e436b3da8630fabfd92fd9a97ea0649187 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 26 Feb 2025 18:29:23 +0100 Subject: [PATCH 139/324] Feat: Logger adjustments, WebSocket closing and more --- src/core/utils/logger.ts | 57 ++++++++++++++-------------------------- 1 file changed, 20 insertions(+), 37 deletions(-) diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts index c704d0a..b68a02f 100644 --- a/src/core/utils/logger.ts +++ b/src/core/utils/logger.ts @@ -1,7 +1,5 @@ import { createLogger, format, transports } from "winston"; -import Transport from "winston-transport"; import path from "path"; -import { dbFunctions } from "~/core/database/repository"; import chalk, { ChalkInstance } from "chalk"; const fileLineFormat = format((info) => { @@ -29,44 +27,29 @@ const fileLineFormat = format((info) => { return info; }); -class SQLiteTransport extends Transport { - constructor(opts?: Transport.TransportStreamOptions) { - super(opts); - } - - log(info: any, callback: () => void) { - const { level, message, file, line } = info; - dbFunctions.addLogEntry(level, message, file || "unknown", line || 0); - callback(); - } -} - export const logger = createLogger({ level: "debug", - format: format.combine(fileLineFormat(), format.json()), - transports: [ - new transports.Console({ - format: format.combine( - format.printf(({ level, message, file, line }) => { - const levelColors: { [key: string]: ChalkInstance } = { - error: chalk.red.bold, - warn: chalk.yellow.bold, - info: chalk.green.bold, - debug: chalk.blue.bold, - verbose: chalk.cyan.bold, - silly: chalk.magenta.bold, - }; - - const paddedLevel = level.toUpperCase(); - const coloredLevel = (levelColors[level] || chalk.white)(paddedLevel); + format: format.combine( + format.timestamp({ format: "DD/MM HH:mm:ss" }), + fileLineFormat(), + format.printf(({ timestamp, level, message, file, line }) => { + const levelColors: { [key: string]: ChalkInstance } = { + error: chalk.red.bold, + warn: chalk.yellow.bold, + info: chalk.green.bold, + debug: chalk.blue.bold, + verbose: chalk.cyan.bold, + silly: chalk.magenta.bold, + }; - const coloredContext = chalk.cyan(`${file}:${line}`); - const coloredMessage = chalk.gray(message); + const paddedLevel = level.toUpperCase().padEnd(5); + const coloredLevel = (levelColors[level] || chalk.white)(paddedLevel); + const coloredContext = chalk.cyan(`${file}:${line}`); + const coloredMessage = chalk.gray(message); + const coloredTimestamp = chalk.yellow(`${timestamp}`); - return `${coloredLevel} [ ${coloredContext} ] - ${coloredMessage}`; - }), - ), + return `${coloredLevel} [ ${coloredTimestamp} ] - ${coloredMessage} - [ ${coloredContext} ]`; }), - new SQLiteTransport(), - ], + ), + transports: [new transports.Console()], }); From 36e1dc28729cf78b874e87871cf89d7abcb9438c Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 26 Feb 2025 19:27:13 +0100 Subject: [PATCH 140/324] Fix: Log level adjustment --- src/routes/docker-websocket.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/docker-websocket.ts b/src/routes/docker-websocket.ts index 06ea1be..f1e6e68 100644 --- a/src/routes/docker-websocket.ts +++ b/src/routes/docker-websocket.ts @@ -31,7 +31,7 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( (socket as any).streams = []; (socket as any).heartbeat = null; // Add heartbeat reference - logger.debug(`Opened WebSocket (${socket.id})`); + logger.info(`Opened WebSocket (${socket.id})`); try { hosts = dbFunctions.getDockerHosts(); From b1559c8cc320b1cccab6b361f5d5d1f4da6953b2 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 27 Feb 2025 21:13:32 +0100 Subject: [PATCH 141/324] Feat: Scheduling, Plugins (wip), databse saving of container stats --- .gitignore | 1 + package.json | 6 +- src/core/database/repository.ts | 153 +++++++++++++++++++---- src/core/docker/scheduler.ts | 72 +++++++++++ src/core/docker/store-container-stats.ts | 77 ++++++++++++ src/core/plugins/loader.ts | 21 +++- src/core/plugins/plugin-actions.ts | 10 ++ src/core/plugins/plugin-manager.ts | 13 +- src/core/utils/change-me-checker.ts | 16 +++ src/core/utils/logger.ts | 12 ++ src/core/utils/type-check.ts | 28 ----- src/index.ts | 13 +- src/plugins/telegram.plugin.ts | 33 +++++ src/routes/api-config.ts | 10 +- src/typings/database.ts | 2 + 15 files changed, 398 insertions(+), 69 deletions(-) create mode 100644 src/core/docker/scheduler.ts create mode 100644 src/core/docker/store-container-stats.ts create mode 100644 src/core/plugins/plugin-actions.ts create mode 100644 src/core/utils/change-me-checker.ts delete mode 100644 src/core/utils/type-check.ts create mode 100644 src/plugins/telegram.plugin.ts diff --git a/.gitignore b/.gitignore index 4bc7b0a..138ece6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. *.db +*.db-journal # dependencies /node_modules diff --git a/package.json b/package.json index ff72d90..515c301 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,6 @@ }, "dependencies": { "@elysiajs/swagger": "^1.2.2", - "@types/dockerode": "^3.3.34", - "@types/split2": "^4.2.3", "chalk": "^5.4.1", "dockerode": "^4.0.4", "elysia": "latest", @@ -17,7 +15,9 @@ "winston-transport": "^4.9.0" }, "devDependencies": { - "bun-types": "latest" + "bun-types": "latest", + "@types/dockerode": "^3.3.34", + "@types/split2": "^4.2.3" }, "module": "src/index.js", "trustedDependencies": [ diff --git a/src/core/database/repository.ts b/src/core/database/repository.ts index 6c0a62b..12c7f81 100644 --- a/src/core/database/repository.ts +++ b/src/core/database/repository.ts @@ -1,7 +1,5 @@ import Database from "bun:sqlite"; import { logger } from "~/core/utils/logger"; -import { typeCheck } from "~/core/utils/type-check"; -import { config } from "~/typings/database"; import type { DockerHost } from "~/typings/docker"; const db = new Database("dockstatapi.db"); @@ -15,8 +13,22 @@ export const dbFunctions = { secure BOOLEAN ); + CREATE TABLE IF NOT EXISTS container_stats ( + id TEXT, + hostId TEXT, + name TEXT, + image TEXT, + status TEXT, + state TEXT, + cpu_usage FLOAT, + memory_usage FLOAT, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP + ); + CREATE TABLE IF NOT EXISTS config ( - polling_rate NUMBER + polling_rate NUMBER, + keep_data_for NUMBER, + fetching_interval NUMBER ); CREATE TABLE IF NOT EXISTS backend_log_entries ( @@ -28,25 +40,42 @@ export const dbFunctions = { ); `); + /* + * Default values: + * - Websocket polling interval 5 seconds + * - Data retention value for the database (logs and container stats) 7 days + * - Data fetcher for the Database: 5 minutes + */ const configRow = db .prepare(`SELECT COUNT(*) AS count FROM config`) .get() as { count: number }; if (configRow.count === 0) { const stmt = db.prepare( ` - INSERT INTO config (polling_rate) VALUES (5) + INSERT INTO config (polling_rate, keep_data_for, fetching_interval) VALUES (5, 7, 5) `, ); - stmt.run(); } + + const hostRow = db + .prepare(`SELECT COUNT(*) AS count FROM docker_hosts WHERE name = ?`) + .get("Localhost") as { count: number }; + if (hostRow.count === 0) { + const stmt = db.prepare( + ` + INSERT INTO docker_hosts (name, url, secure) VALUES (?, ?, ?) + `, + ); + stmt.run("Localhost", "localhost:2375", false); + } }, addDockerHost(hostId: string, url: string, secure: boolean) { if ( - !typeCheck(hostId, "string") || - !typeCheck(url, "string") || - !typeCheck(secure, "boolean") + typeof hostId !== "string" || + typeof url !== "string" || + typeof secure !== "boolean" ) { logger.crit("Invalid parameter types for addDockerHost"); throw new TypeError("Invalid parameter types for addDockerHost"); @@ -76,10 +105,10 @@ export const dbFunctions = { line: number, ) => { if ( - !typeCheck(level, "string") || - !typeCheck(message, "string") || - !typeCheck(file_name, "string") || - !typeCheck(line, "number") + typeof level !== "string" || + typeof message !== "string" || + typeof file_name !== "string" || + typeof line !== "number" ) { logger.crit("Invalid parameter types for addLogEntry"); throw new TypeError("Invalid parameter types for addLogEntry"); @@ -102,7 +131,7 @@ export const dbFunctions = { }, getLogsByLevel(level: string) { - if (!typeCheck(level, "string")) { + if (typeof level !== "string") { logger.crit("Level parameter must be a string"); throw new TypeError("Level parameter must be a string"); } @@ -118,9 +147,9 @@ export const dbFunctions = { updateDockerHost(name: string, url: string, secure: boolean) { if ( - !typeCheck(name, "string") || - !typeCheck(url, "string") || - !typeCheck(secure, "boolean") + typeof name !== "string" || + typeof url !== "string" || + typeof secure !== "boolean" ) { logger.crit("Invalid parameter types for updateDockerHost"); throw new TypeError("Invalid parameter types for updateDockerHost"); @@ -135,7 +164,7 @@ export const dbFunctions = { }, deleteDockerHost(name: string) { - if (!typeCheck(name, "string")) { + if (typeof name !== "string") { logger.crit("Invalid parameter type for deleteDockerHost"); throw new TypeError("Name parameter must be a string"); } @@ -155,7 +184,7 @@ export const dbFunctions = { }, clearLogsByLevel(level: string) { - if (!typeCheck(level, "string")) { + if (typeof level !== "string") { logger.crit("Invalid parameter type for clearLogsByLevel"); throw new TypeError("Level parameter must be a string"); } @@ -167,28 +196,98 @@ export const dbFunctions = { return stmt.run(level); }, - updateConfig(polling_rate: number) { - if (!typeCheck(polling_rate, "number")) { - logger.crit("Invalid parameter type for updateConfig"); - throw new TypeError("Polling rate must be a number!"); + updateConfig( + polling_rate: number, + fetching_interval: number, + keep_data_for: number, + ) { + if ( + typeof polling_rate !== "number" || + typeof fetching_interval !== "number" || + typeof keep_data_for !== "number" + ) { + logger.crit("Invalid parameter types for updateConfig"); + throw new TypeError("Invalid parameter types for updateConfig"); } const stmt = db.prepare(` - UPDATE config - SET polling_rate = ? - `); + UPDATE config + SET polling_rate = ?, + fetching_interval = ?, + keep_data_for = ? + `); - return stmt.run(polling_rate); + return stmt.run(polling_rate, fetching_interval, keep_data_for); }, getConfig() { const stmt = db.prepare(` - SELECT distinct(polling_rate) + SELECT polling_rate, keep_data_for, fetching_interval FROM config `); return stmt.all(); }, + + // Stats: + addContainerStats( + id: string, + hostId: string, + name: string, + image: string, + status: string, + state: string, + cpu_usage: number, + memory_usage: number, + ) { + if ( + typeof id !== "string" || + typeof hostId !== "string" || + typeof name !== "string" || + typeof image !== "string" || + typeof status !== "string" || + typeof state !== "string" || + typeof cpu_usage !== "number" || + typeof memory_usage !== "number" + ) { + logger.crit("Invalid parameter types for addContainerStats"); + throw new TypeError("Invalid parameter types for addContainerStats"); + } + + const stmt = db.prepare(` + INSERT INTO container_stats (id, hostId, name, image, status, state, cpu_usage, memory_usage) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `); + return stmt.run( + id, + hostId, + name, + image, + status, + state, + cpu_usage, + memory_usage, + ); + }, + + deleteOldData(days: number) { + if (typeof days !== "number") { + logger.crit("Invalid parameter type for deleteOldData"); + throw new TypeError("Days parameter must be a number"); + } + + const deleteContainerStmt = db.prepare(` + DELETE FROM container_stats + WHERE timestamp < datetime('now', '-' || ? || ' days') + `); + deleteContainerStmt.run(days); + + const deleteLogsStmt = db.prepare(` + DELETE FROM backend_log_entries + WHERE timestamp < datetime('now', '-' || ? || ' days') + `); + deleteLogsStmt.run(days); + }, }; dbFunctions.init(); diff --git a/src/core/docker/scheduler.ts b/src/core/docker/scheduler.ts new file mode 100644 index 0000000..14d4118 --- /dev/null +++ b/src/core/docker/scheduler.ts @@ -0,0 +1,72 @@ +import storeContainerData from "~/core/docker/store-container-stats"; +import { dbFunctions } from "../database/repository"; +import { config } from "~/typings/database"; +import { logger } from "~/core/utils/logger"; + +function convertFromMinToMs(minutes: number): number { + return minutes * 60 * 1000; +} + +async function setSchedules() { + try { + const rawConfigData: unknown[] = dbFunctions.getConfig(); + const configData = rawConfigData[0]; + + if ( + !configData || + typeof (configData as config).keep_data_for !== "number" || + typeof (configData as config).fetching_interval !== "number" + ) { + logger.error("Invalid configuration data:", configData); + throw new Error("Invalid configuration data"); + } + + const { keep_data_for, fetching_interval } = configData as config; + + if (keep_data_for === undefined) { + const errMsg = "keep_data_for is undefined"; + logger.error(errMsg); + throw new Error(errMsg); + } + + if (fetching_interval === undefined) { + const errMsg = "fetching_interval is undefined"; + logger.error(errMsg); + throw new Error(errMsg); + } + + logger.info( + `Scheduling: Fetching container statistics every ${fetching_interval} minutes`, + ); + logger.info(`Scheduling: Cleaning up Database every ${keep_data_for} days`); + + // Schedule container data fetching + setInterval(async () => { + try { + logger.info("Task Start: Fetching container data."); + await storeContainerData(); + logger.info("Task End: Container data fetched successfully."); + } catch (error) { + logger.error("Error in fetching container data:", error); + } + }, convertFromMinToMs(fetching_interval)); + + // Schedule database cleanup + setInterval(() => { + try { + logger.info("Task Start: Cleaning up old database data."); + dbFunctions.deleteOldData(keep_data_for); + logger.info("Task End: Database cleanup completed."); + } catch (error) { + logger.error("Error in database cleanup task:", error); + } + }, convertFromMinToMs(60)); + + logger.info("Schedules have been set successfully."); + } catch (error) { + logger.error("Error setting schedules:", error); + throw error; + } +} + +export { setSchedules }; diff --git a/src/core/docker/store-container-stats.ts b/src/core/docker/store-container-stats.ts new file mode 100644 index 0000000..e39d937 --- /dev/null +++ b/src/core/docker/store-container-stats.ts @@ -0,0 +1,77 @@ +import { getDockerClient } from "~/core/docker/client"; +import { dbFunctions } from "~/core/database/repository"; +import Docker from "dockerode"; +import { + calculateCpuPercent, + calculateMemoryUsage, +} from "~/core/utils/calculations"; + +async function storeContainerData() { + try { + // Stage 1: getting all docker hosts and mapping over them + const hosts = dbFunctions.getDockerHosts(); + + hosts.map(async (host) => { + try { + // Stage 2: getting the Docker client and pinging to test the connection + const docker = getDockerClient(host); + + try { + await docker.ping(); + } catch (error) { + throw new Error( + `Error while pinging docker host: ${error as string}`, + ); + } + + const containers = await docker.listContainers({ all: true }); + + await Promise.all( + containers.map(async (containerInfo) => { + try { + const container = docker.getContainer(containerInfo.Id); + const stats = await new Promise( + (resolve, reject) => { + container.stats({ stream: false }, (error, stats) => { + if (error) { + return reject( + new Error(`An Error occured: ${error as string}`), + ); + } + if (!stats) { + return reject( + new Error(`No Stats available: ${error as string}`), + ); + } + resolve(stats); + }); + }, + ); + + dbFunctions.addContainerStats( + containerInfo.Id, + host.name, + containerInfo.Names[0].replace(/^\//, ""), + containerInfo.Image, + containerInfo.Status, + containerInfo.State, + calculateCpuPercent(stats), + calculateMemoryUsage(stats), + ); + } catch (error) { + throw new Error(`An error occurred: ${error as string}`); + } + }), + ); + } catch (error: unknown) { + throw new Error( + `Error while getting docker client: ${error as string}`, + ); + } + }); + } catch (error: unknown) { + throw new Error("Error while XXX"); + } +} + +export default storeContainerData; diff --git a/src/core/plugins/loader.ts b/src/core/plugins/loader.ts index 40f79c4..26d00e5 100644 --- a/src/core/plugins/loader.ts +++ b/src/core/plugins/loader.ts @@ -1,21 +1,36 @@ import { pluginManager } from "./plugin-manager"; import path from "path"; import fs from "fs"; +import { logger } from "../utils/logger"; +import { checkFileForChangeMe } from "../utils/change-me-checker"; export async function loadPlugins(pluginDir: string) { const pluginPath = path.join(process.cwd(), pluginDir); + logger.debug(`Loading plugins (${pluginPath})`); if (!fs.existsSync(pluginPath)) { return; } + let pluginCount = 0; const files = fs.readdirSync(pluginPath); for (const file of files) { if (!file.endsWith(".plugin.ts")) continue; - const module = await import(path.join(pluginPath, file)); - const plugin = module.default; - pluginManager.register(plugin); + const absolutePath = path.join(pluginPath, file); + try { + await checkFileForChangeMe(absolutePath); + const module = await import(absolutePath); + const plugin = module.default; + pluginManager.register(plugin); + pluginCount++; + } catch (error) { + logger.error( + `Error while importing plugin ${absolutePath}: ${error as string}`, + ); + } } + + logger.info(`Registered ${pluginCount} plugin(s)`); } diff --git a/src/core/plugins/plugin-actions.ts b/src/core/plugins/plugin-actions.ts new file mode 100644 index 0000000..0b2f935 --- /dev/null +++ b/src/core/plugins/plugin-actions.ts @@ -0,0 +1,10 @@ +import { pluginManager } from "./plugin-manager"; + +export const pluginAction = { + containerStart(containerInfo: any) { + pluginManager.handleContainerStart(containerInfo); + }, + metricsReceived(metrics: any) { + pluginManager.handleMetrics(metrics); + }, +}; diff --git a/src/core/plugins/plugin-manager.ts b/src/core/plugins/plugin-manager.ts index 15d66f4..2a9b131 100644 --- a/src/core/plugins/plugin-manager.ts +++ b/src/core/plugins/plugin-manager.ts @@ -1,4 +1,5 @@ import { EventEmitter } from "events"; +import { logger } from "../utils/logger"; export interface Plugin { name: string; @@ -11,14 +12,22 @@ export class PluginManager extends EventEmitter { private plugins: Map = new Map(); register(plugin: Plugin) { - this.plugins.set(plugin.name, plugin); - console.log(`Registered plugin: ${plugin.name}`); + try { + this.plugins.set(plugin.name, plugin); + logger.debug(`Registered plugin: ${plugin.name}`); + } catch (error) { + logger.error( + `Registering plugin ${plugin.name} failed: ${error as string}`, + ); + } } unregister(name: string) { this.plugins.delete(name); } + // Trigger plugin flows: + handleContainerStart(containerInfo: any) { this.plugins.forEach((plugin) => { plugin.onContainerStart?.(containerInfo); diff --git a/src/core/utils/change-me-checker.ts b/src/core/utils/change-me-checker.ts new file mode 100644 index 0000000..fa19520 --- /dev/null +++ b/src/core/utils/change-me-checker.ts @@ -0,0 +1,16 @@ +import { readFile } from "fs/promises"; +import { logger } from "~/core/utils/logger"; + +export async function checkFileForChangeMe(filePath: string) { + const regex = /change[\W_]*me/i; + let content = ""; + try { + content = await readFile(filePath, "utf-8"); + } catch (error) { + logger.error("Error reading file:", error); + } + + if (regex.test(content)) { + throw new Error(`Error: The file contains 'CHANGE_ME'. Please update it.`); + } +} diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts index b68a02f..a173cb7 100644 --- a/src/core/utils/logger.ts +++ b/src/core/utils/logger.ts @@ -1,6 +1,7 @@ import { createLogger, format, transports } from "winston"; import path from "path"; import chalk, { ChalkInstance } from "chalk"; +import { dbFunctions } from "../database/repository"; const fileLineFormat = format((info) => { try { @@ -48,6 +49,17 @@ export const logger = createLogger({ const coloredMessage = chalk.gray(message); const coloredTimestamp = chalk.yellow(`${timestamp}`); + try { + dbFunctions.addLogEntry( + level, + message as string, + file as string, + line as number, + ); + } catch (error) { + logger.error(`Error inserting log into DB: ${error as string}`); + } + return `${coloredLevel} [ ${coloredTimestamp} ] - ${coloredMessage} - [ ${coloredContext} ]`; }), ), diff --git a/src/core/utils/type-check.ts b/src/core/utils/type-check.ts deleted file mode 100644 index 8675f79..0000000 --- a/src/core/utils/type-check.ts +++ /dev/null @@ -1,28 +0,0 @@ -type TypeCheck = [any, string]; - -export function typeCheck(value: any, expectedType: string): boolean { - if (expectedType === "null") { - return value === null; - } - - if (expectedType === "array") { - return Array.isArray(value); - } - - const actualType = typeof value; - - if (actualType === "object" && value !== null) { - if (expectedType === "object") { - return !Array.isArray(value); - } - return false; - } - - return actualType === expectedType; -} - -export function validateTypes(checks: TypeCheck[]): boolean[] { - return checks.map(([value, expectedType]) => { - return typeCheck(value, expectedType.toLowerCase()); - }); -} diff --git a/src/index.ts b/src/index.ts index 06e500d..90e7367 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,10 +8,13 @@ import { dockerStatsRoutes } from "~/routes/docker-stats"; import { backendLogs } from "~/routes/logs"; import { dockerWebsocketRoutes } from "~/routes/docker-websocket"; import { apiConfigRoutes } from "~/routes/api-config"; +import { setSchedules } from "~/core/docker/scheduler"; + +logger.info("Starting server..."); dbFunctions.init(); -const app = new Elysia() +const DockStatAPI = new Elysia() .use( swagger({ documentation: { @@ -47,9 +50,9 @@ const app = new Elysia() async function startServer() { try { - await loadPlugins("./plugins"); + await loadPlugins("./src/plugins"); - app.listen(3000, ({ hostname, port }) => { + DockStatAPI.listen(3000, ({ hostname, port }) => { logger.info(`DockStatAPI is running at http://${hostname}:${port}`); logger.info( `Swagger API Documentation available at http://${hostname}:${port}/swagger`, @@ -61,4 +64,6 @@ async function startServer() { } } -startServer(); +await startServer(); +await setSchedules(); +logger.info("Started server"); diff --git a/src/plugins/telegram.plugin.ts b/src/plugins/telegram.plugin.ts new file mode 100644 index 0000000..1af70f8 --- /dev/null +++ b/src/plugins/telegram.plugin.ts @@ -0,0 +1,33 @@ +import type { Plugin } from "~/core/plugins/plugin-manager"; +import { logger } from "~/core/utils/logger"; + +const TELEGRAM_BOT_TOKEN = "CHANGE_ME"; // Replace with your bot token +const TELEGRAM_CHAT_ID = "CHANGE_ME"; // Replace with your chat ID + +const TelegramNotificationPlugin: Plugin = { + name: "Telegram Notification Plugin", + async onContainerStart(containerName: string) { + const message = `Container Started: ${containerName}`; + try { + const response = await fetch( + `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + chat_id: TELEGRAM_CHAT_ID, + text: message, + }), + }, + ); + if (!response.ok) { + logger.error(`HTTP error ${response.status}`); + } + logger.info("Telegram notification sent."); + } catch (error) { + logger.error("Failed to send Telegram notification", error as string); + } + }, +}; + +export default TelegramNotificationPlugin; diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index 41262c8..a77d2b4 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -31,9 +31,13 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) "/update", async ({ set, body }) => { try { - const { polling_rate } = body; + const { polling_rate, fetching_interval, keep_data_for } = body; set.headers["Content-Type"] = "application/json"; - dbFunctions.updateConfig(polling_rate); + dbFunctions.updateConfig( + polling_rate, + fetching_interval, + keep_data_for, + ); return responseHandler.ok(set, "Updated DockStatAPI config"); } catch (error) { return responseHandler.error( @@ -46,6 +50,8 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) { body: t.Object({ polling_rate: t.Number(), + fetching_interval: t.Number(), + keep_data_for: t.Number(), }), tags: ["Management"], }, diff --git a/src/typings/database.ts b/src/typings/database.ts index d39ccbf..425fa46 100644 --- a/src/typings/database.ts +++ b/src/typings/database.ts @@ -8,6 +8,8 @@ interface backend_log_entries { interface config { polling_rate: number; + keep_data_for: number; + fetching_interval: number; } export type { backend_log_entries, config }; From e0352f971e5c197f94be55c0087b1ac780285515 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 28 Feb 2025 21:17:32 +0100 Subject: [PATCH 142/324] Feat: README update, Storing host stats in db, plugin functions and more --- README.md | 69 +++++++++- src/core/database/repository.ts | 69 ++++++++++ src/core/docker/scheduler.ts | 47 ++++++- src/core/docker/store-container-stats.ts | 57 +++++--- src/core/docker/store-host-stats.ts | 68 ++++++++++ src/core/plugins/plugin-manager.ts | 76 +++++++++-- src/plugins/example.plugin.ts | 30 +++-- src/plugins/telegram.plugin.ts | 9 +- src/routes/docker-stats.ts | 13 +- src/typings/docker.ts | 10 +- src/typings/dockerode.ts | 162 +++++++++++++++++++++++ src/typings/plugin.ts | 25 ++++ 12 files changed, 583 insertions(+), 52 deletions(-) create mode 100644 src/core/docker/store-host-stats.ts create mode 100644 src/typings/dockerode.ts create mode 100644 src/typings/plugin.ts diff --git a/README.md b/README.md index 6cc99af..eb71a1e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,68 @@ -# REWRITE +# DockStat API -Using Bun, keep an eye out! +! WIP Documentation ! + +## Usage + +The DockStat API provides the following endpoints: + +### Docker Containers +- `GET /docker/containers`: Retrieve statistics for all containers across all configured Docker hosts. + +### Docker Hosts +- `GET /docker/hosts/:id`: Retrieve configuration and statistics for a specific Docker host. + +### Docker Configuration +- `POST /docker-config/add-host`: Add a new Docker host. +- `POST /docker-config/update-host`: Update an existing Docker host. +- `GET /docker-config/hosts`: Retrieve a list of all configured Docker hosts. + +### API Configuration +- `GET /config/get`: Retrieve the current API configuration. +- `POST /config/update`: Update the API configuration. + +### Logs +- `GET /logs`: Retrieve all backend logs. +- `GET /logs/:level`: Retrieve logs filtered by log level. +- `DELETE /logs`: Clear all backend logs. +- `DELETE /logs/:level`: Clear logs by log level. + +## API + +The DockStat API exposes the following endpoints: + +| Endpoint | Method | Description | +| --- | --- | --- | +| `/docker/containers` | `GET` | Retrieve statistics for all containers across all configured Docker hosts. | +| `/docker/hosts/:id` | `GET` | Retrieve configuration and statistics for a specific Docker host. | +| `/docker-config/add-host` | `POST` | Add a new Docker host. | +| `/docker-config/update-host` | `POST` | Update an existing Docker host. | +| `/docker-config/hosts` | `GET` | Retrieve a list of all configured Docker hosts. | +| `/config/get` | `GET` | Retrieve the current API configuration. | +| `/config/update` | `POST` | Update the API configuration. | +| `/logs` | `GET` | Retrieve all backend logs. | +| `/logs/:level` | `GET` | Retrieve logs filtered by log level. | +| `/logs` | `DELETE` | Clear all backend logs. | +| `/logs/:level` | `DELETE` | Clear logs by log level. | + +## Contributing + +1. Fork the repository. +2. Create a new branch for your feature or bug fix. +3. Make your changes and commit them. +4. Push your branch to your forked repository. +5. Submit a pull request to the main repository. + +## License + +This project is licensed under the [MIT License](LICENSE). + +## Testing + +To run the tests, execute the following command: +(Currently no tests configured!) +``` +bun test +``` + +This will run the test suite and report the results. diff --git a/src/core/database/repository.ts b/src/core/database/repository.ts index 12c7f81..7c60a5d 100644 --- a/src/core/database/repository.ts +++ b/src/core/database/repository.ts @@ -1,6 +1,7 @@ import Database from "bun:sqlite"; import { logger } from "~/core/utils/logger"; import type { DockerHost } from "~/typings/docker"; +import type { HostStats } from "~/typings/docker"; const db = new Database("dockstatapi.db"); @@ -13,6 +14,22 @@ export const dbFunctions = { secure BOOLEAN ); + CREATE TABLE IF NOT EXISTS host_stats ( + hostId TEXT PRIMARY KEY, + dockerVersion TEXT, + apiVersion TEXT, + os TEXT, + architecture TEXT, + totalMemory INTEGER, + totalCPU INTEGER, + labels TEXT, + containers INTEGER, + containersRunning INTEGER, + containersStopped INTEGER, + containersPaused INTEGER, + images INTEGER + ); + CREATE TABLE IF NOT EXISTS container_stats ( id TEXT, hostId TEXT, @@ -50,6 +67,7 @@ export const dbFunctions = { .prepare(`SELECT COUNT(*) AS count FROM config`) .get() as { count: number }; if (configRow.count === 0) { + logger.debug("Initializing default config"); const stmt = db.prepare( ` INSERT INTO config (polling_rate, keep_data_for, fetching_interval) VALUES (5, 7, 5) @@ -62,6 +80,7 @@ export const dbFunctions = { .prepare(`SELECT COUNT(*) AS count FROM docker_hosts WHERE name = ?`) .get("Localhost") as { count: number }; if (hostRow.count === 0) { + logger.debug("Initializing default docker host (Localhost)"); const stmt = db.prepare( ` INSERT INTO docker_hosts (name, url, secure) VALUES (?, ?, ?) @@ -288,6 +307,56 @@ export const dbFunctions = { `); deleteLogsStmt.run(days); }, + + updateHostStats(stats: HostStats) { + const labelsJson = JSON.stringify(stats.labels); + const stmt = db.prepare(` + INSERT INTO host_stats ( + hostId, + dockerVersion, + apiVersion, + os, + architecture, + totalMemory, + totalCPU, + labels, + containers, + containersRunning, + containersStopped, + containersPaused, + images + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(hostId) DO UPDATE SET + dockerVersion = excluded.dockerVersion, + apiVersion = excluded.apiVersion, + os = excluded.os, + architecture = excluded.architecture, + totalMemory = excluded.totalMemory, + totalCPU = excluded.totalCPU, + labels = excluded.labels, + containers = excluded.containers, + containersRunning = excluded.containersRunning, + containersStopped = excluded.containersStopped, + containersPaused = excluded.containersPaused, + images = excluded.images; + `); + return stmt.run( + stats.hostId, + stats.dockerVersion, + stats.apiVersion, + stats.os, + stats.architecture, + stats.totalMemory, + stats.totalCPU, + labelsJson, + stats.containers, + stats.containersRunning, + stats.containersStopped, + stats.containersPaused, + stats.images, + ); + }, }; dbFunctions.init(); diff --git a/src/core/docker/scheduler.ts b/src/core/docker/scheduler.ts index 14d4118..e4adf9c 100644 --- a/src/core/docker/scheduler.ts +++ b/src/core/docker/scheduler.ts @@ -1,12 +1,30 @@ import storeContainerData from "~/core/docker/store-container-stats"; -import { dbFunctions } from "../database/repository"; +import { dbFunctions } from "~/core/database/repository"; import { config } from "~/typings/database"; import { logger } from "~/core/utils/logger"; +import storeHostData from "~/core/docker//store-host-stats"; function convertFromMinToMs(minutes: number): number { return minutes * 60 * 1000; } +async function initialRun( + scheduleName: string, + scheduleFunction: Promise | void, + isAsync: boolean, +) { + try { + if (isAsync) { + await scheduleFunction; + } else { + scheduleFunction; + } + logger.info(`Startup run success for: ${scheduleName}`); + } catch (error) { + logger.error(`Startup run failed for ${scheduleName}, ${error as string}`); + } +} + async function setSchedules() { try { const rawConfigData: unknown[] = dbFunctions.getConfig(); @@ -38,9 +56,17 @@ async function setSchedules() { logger.info( `Scheduling: Fetching container statistics every ${fetching_interval} minutes`, ); - logger.info(`Scheduling: Cleaning up Database every ${keep_data_for} days`); + + logger.info( + `Scheduling: Updating host statistics every ${fetching_interval} minutes`, + ); + + logger.info( + `Scheduling: Cleaning up Database every hour and deleting data older then ${keep_data_for} days`, + ); // Schedule container data fetching + await initialRun("storeContainerData", storeContainerData(), true); setInterval(async () => { try { logger.info("Task Start: Fetching container data."); @@ -51,7 +77,24 @@ async function setSchedules() { } }, convertFromMinToMs(fetching_interval)); + // Schedule Host statistics updates + await initialRun("storeHostData", storeHostData(), true); + setInterval(async () => { + try { + logger.info("Task Start: Updating host stats."); + await storeHostData(); + logger.info("Task End: Updating host stats successfully."); + } catch (error) { + logger.error("Error in updating host stats:", error); + } + }, convertFromMinToMs(fetching_interval)); + // Schedule database cleanup + await initialRun( + "dbFunctions.deleteOldData", + dbFunctions.deleteOldData(keep_data_for), + false, + ); setInterval(() => { try { logger.info("Task Start: Cleaning up old database data."); diff --git a/src/core/docker/store-container-stats.ts b/src/core/docker/store-container-stats.ts index e39d937..e64f31b 100644 --- a/src/core/docker/store-container-stats.ts +++ b/src/core/docker/store-container-stats.ts @@ -8,39 +8,57 @@ import { async function storeContainerData() { try { - // Stage 1: getting all docker hosts and mapping over them const hosts = dbFunctions.getDockerHosts(); - hosts.map(async (host) => { - try { - // Stage 2: getting the Docker client and pinging to test the connection + // Process each host concurrently and wait for them all to finish + await Promise.all( + hosts.map(async (host) => { const docker = getDockerClient(host); + // Test the connection with a ping try { await docker.ping(); } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); throw new Error( - `Error while pinging docker host: ${error as string}`, + `Failed to ping docker host "${host.name}": ${errMsg}`, ); } - const containers = await docker.listContainers({ all: true }); + let containers; + try { + containers = await docker.listContainers({ all: true }); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error( + `Failed to list containers on host "${host.name}": ${errMsg}`, + ); + } + // Process each container concurrently await Promise.all( containers.map(async (containerInfo) => { + const containerName = containerInfo.Names[0].replace(/^\//, ""); try { const container = docker.getContainer(containerInfo.Id); - const stats = await new Promise( + + const stats: Docker.ContainerStats = await new Promise( (resolve, reject) => { container.stats({ stream: false }, (error, stats) => { if (error) { + const errMsg = + error instanceof Error ? error.message : String(error); return reject( - new Error(`An Error occured: ${error as string}`), + new Error( + `Failed to get stats for container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}": ${errMsg}`, + ), ); } if (!stats) { return reject( - new Error(`No Stats available: ${error as string}`), + new Error( + `No stats returned for container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}".`, + ), ); } resolve(stats); @@ -51,7 +69,7 @@ async function storeContainerData() { dbFunctions.addContainerStats( containerInfo.Id, host.name, - containerInfo.Names[0].replace(/^\//, ""), + containerName, containerInfo.Image, containerInfo.Status, containerInfo.State, @@ -59,18 +77,19 @@ async function storeContainerData() { calculateMemoryUsage(stats), ); } catch (error) { - throw new Error(`An error occurred: ${error as string}`); + const errMsg = + error instanceof Error ? error.message : String(error); + throw new Error( + `Error processing container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}": ${errMsg}`, + ); } }), ); - } catch (error: unknown) { - throw new Error( - `Error while getting docker client: ${error as string}`, - ); - } - }); - } catch (error: unknown) { - throw new Error("Error while XXX"); + }), + ); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to store container data: ${errMsg}`); } } diff --git a/src/core/docker/store-host-stats.ts b/src/core/docker/store-host-stats.ts new file mode 100644 index 0000000..f8cd352 --- /dev/null +++ b/src/core/docker/store-host-stats.ts @@ -0,0 +1,68 @@ +import { logger } from "~/core/utils/logger"; +import { dbFunctions } from "~/core/database/repository"; +import { DockerHost, HostStats } from "~/typings/docker"; +import { getDockerClient } from "~/core/docker/client"; +import { DockerInfo } from "~/typings/dockerode"; + +async function storeHostData() { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + + await Promise.all( + hosts.map(async (host) => { + const docker = getDockerClient(host); + + try { + await docker.ping(); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error( + `Failed to ping docker host "${host.name}": ${errMsg}`, + ); + } + + let hostStats: DockerInfo; + let stats: HostStats; + try { + hostStats = await docker.info(); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error( + `Failed to fetch stats for host "${host.name}": ${errMsg}`, + ); + } + + try { + const stats: HostStats = { + hostId: host.name, + dockerVersion: hostStats.ServerVersion, + apiVersion: hostStats.Driver, + os: hostStats.OperatingSystem, + architecture: hostStats.Architecture, + totalMemory: hostStats.MemTotal, + totalCPU: hostStats.NCPU, + labels: hostStats.Labels, + images: hostStats.Images, + containers: hostStats.Containers, + containersPaused: hostStats.ContainersPaused, + containersRunning: hostStats.ContainersRunning, + containersStopped: hostStats.ContainersStopped, + }; + + dbFunctions.updateHostStats(stats); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error( + `Failed to store stats for host "${host.name}": ${errMsg}`, + ); + } + }), + ); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + logger.error(`storeHostData failed: ${errMsg}`); + throw new Error(`Failed to store host data: ${errMsg}`); + } +} + +export default storeHostData; diff --git a/src/core/plugins/plugin-manager.ts b/src/core/plugins/plugin-manager.ts index 2a9b131..614604a 100644 --- a/src/core/plugins/plugin-manager.ts +++ b/src/core/plugins/plugin-manager.ts @@ -1,12 +1,7 @@ import { EventEmitter } from "events"; import { logger } from "../utils/logger"; - -export interface Plugin { - name: string; - onContainerStart?: (containerInfo: any) => void; - onMetricsReceived?: (metrics: any) => void; - onLogReceived?: (log: string) => void; -} +import type { Plugin } from "~/typings/plugin"; +import type { ContainerInfo, HostStats } from "~/typings/docker"; export class PluginManager extends EventEmitter { private plugins: Map = new Map(); @@ -27,16 +22,75 @@ export class PluginManager extends EventEmitter { } // Trigger plugin flows: + handleContainerStop(containerInfo: ContainerInfo) { + this.plugins.forEach((plugin) => { + plugin.onContainerStop?.(containerInfo); + }); + } + + handleContainerExit(containerInfo: ContainerInfo) { + this.plugins.forEach((plugin) => { + plugin.onContainerExit?.(containerInfo); + }); + } + + handleContainerCreate(containerInfo: ContainerInfo) { + this.plugins.forEach((plugin) => { + plugin.onContainerCreate?.(containerInfo); + }); + } + + handleContainerDestroy(containerInfo: ContainerInfo) { + this.plugins.forEach((plugin) => { + plugin.onContainerDestroy?.(containerInfo); + }); + } + + handleContainerPause(containerInfo: ContainerInfo) { + this.plugins.forEach((plugin) => { + plugin.onContainerPause?.(containerInfo); + }); + } + + handleContainerUnpause(containerInfo: ContainerInfo) { + this.plugins.forEach((plugin) => { + plugin.onContainerUnpause?.(containerInfo); + }); + } + + handleContainerRestart(containerInfo: ContainerInfo) { + this.plugins.forEach((plugin) => { + plugin.onContainerRestart?.(containerInfo); + }); + } + + handleContainerUpdate(containerInfo: ContainerInfo) { + this.plugins.forEach((plugin) => { + plugin.onContainerUpdate?.(containerInfo); + }); + } + + handleContainerRename(containerInfo: ContainerInfo) { + this.plugins.forEach((plugin) => { + plugin.onContainerRename?.(containerInfo); + }); + } + + handleContainerHealthStatus(containerInfo: ContainerInfo) { + this.plugins.forEach((plugin) => { + plugin.onContainerHealthStatus?.(containerInfo); + }); + } - handleContainerStart(containerInfo: any) { + handleHostUnreachable(HostStats: HostStats) { this.plugins.forEach((plugin) => { - plugin.onContainerStart?.(containerInfo); + plugin.onHostUnreachable?.(HostStats); }); } - handleMetrics(metrics: any) { + handleHostReachableAgain(HostStats: HostStats) { this.plugins.forEach((plugin) => { - plugin.onMetricsReceived?.(metrics); + plugin.onHostReachableAgain?.(HostStats); }); } } diff --git a/src/plugins/example.plugin.ts b/src/plugins/example.plugin.ts index 48ca11a..a9ed6ac 100644 --- a/src/plugins/example.plugin.ts +++ b/src/plugins/example.plugin.ts @@ -1,11 +1,23 @@ -import { Plugin } from "~/core/plugins/plugin-manager"; +import type { Plugin } from "~/typings/plugin"; +import type { ContainerInfo } from "~/typings/docker"; +import type { HostStats } from "~/typings/docker"; +import { logger } from "~/core/utils/logger"; -export default { - name: "example-plugin", - onContainerStart: (containerInfo) => { - console.log(`Container started: ${containerInfo.id}`); - }, - onMetricsReceived: (metrics) => { - console.log("Received metrics:", metrics); - }, +const ExamplePlugin: Plugin = { + name: "Example Plugin", + async onContainerStart(containerInfo: ContainerInfo) {}, + async onContainerStop(containerInfo: ContainerInfo) {}, + async onContainerExit(containerInfo: ContainerInfo) {}, + async onContainerCreate(containerInfo: ContainerInfo) {}, + async onContainerDestroy(containerInfo: ContainerInfo) {}, + async onContainerPause(containerInfo: ContainerInfo) {}, + async onContainerUnpause(containerInfo: ContainerInfo) {}, + async onContainerRestart(containerInfo: ContainerInfo) {}, + async onContainerUpdate(containerInfo: ContainerInfo) {}, + async onContainerRename(containerInfo: ContainerInfo) {}, + async onContainerHealthStatus(containerInfo: ContainerInfo) {}, + async onHostUnreachable(HostStats: HostStats) {}, + async onHostReachableAgain(HostStats: HostStats) {}, } satisfies Plugin; + +export default ExamplePlugin; diff --git a/src/plugins/telegram.plugin.ts b/src/plugins/telegram.plugin.ts index 1af70f8..cf7c376 100644 --- a/src/plugins/telegram.plugin.ts +++ b/src/plugins/telegram.plugin.ts @@ -1,4 +1,5 @@ -import type { Plugin } from "~/core/plugins/plugin-manager"; +import type { Plugin } from "~/typings/plugin"; +import type { ContainerInfo } from "~/typings/docker"; import { logger } from "~/core/utils/logger"; const TELEGRAM_BOT_TOKEN = "CHANGE_ME"; // Replace with your bot token @@ -6,8 +7,8 @@ const TELEGRAM_CHAT_ID = "CHANGE_ME"; // Replace with your chat ID const TelegramNotificationPlugin: Plugin = { name: "Telegram Notification Plugin", - async onContainerStart(containerName: string) { - const message = `Container Started: ${containerName}`; + async onContainerStart(containerInfo: ContainerInfo) { + const message = `Container Started: ${containerInfo.name} on ${containerInfo.hostId}`; try { const response = await fetch( `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`, @@ -28,6 +29,6 @@ const TelegramNotificationPlugin: Plugin = { logger.error("Failed to send Telegram notification", error as string); } }, -}; +} satisfies Plugin; export default TelegramNotificationPlugin; diff --git a/src/routes/docker-stats.ts b/src/routes/docker-stats.ts index 95e4a8b..d85bfc1 100644 --- a/src/routes/docker-stats.ts +++ b/src/routes/docker-stats.ts @@ -8,7 +8,8 @@ import { } from "~/core/utils/calculations"; import { logger } from "~/core/utils/logger"; import { responseHandler } from "~/core/utils/respone-handler"; -import type { ContainerInfo, DockerHost, HostConfig } from "~/typings/docker"; +import type { ContainerInfo, DockerHost, HostStats } from "~/typings/docker"; +import type { DockerInfo } from "~/typings/dockerode"; export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) .get( @@ -119,9 +120,9 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) } const docker = getDockerClient(host); - const info = await docker.info(); + const info: DockerInfo = await docker.info(); - const config: HostConfig = { + const config: HostStats = { hostId: host.name, dockerVersion: info.ServerVersion, apiVersion: info.Driver, @@ -129,6 +130,12 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) architecture: info.Architecture, totalMemory: info.MemTotal, totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, }; set.headers["Content-Type"] = "application/json"; diff --git a/src/typings/docker.ts b/src/typings/docker.ts index 8d78ae2..522762c 100644 --- a/src/typings/docker.ts +++ b/src/typings/docker.ts @@ -15,7 +15,7 @@ interface ContainerInfo { memoryUsage: number; } -interface HostConfig { +interface HostStats { hostId: string; dockerVersion: string; apiVersion: string; @@ -23,6 +23,12 @@ interface HostConfig { architecture: string; totalMemory: number; totalCPU: number; + labels: string[]; + containers: number; + containersRunning: number; + containersStopped: number; + containersPaused: number; + images: number; } -export type { HostConfig, ContainerInfo, DockerHost }; +export type { HostStats, ContainerInfo, DockerHost }; diff --git a/src/typings/dockerode.ts b/src/typings/dockerode.ts new file mode 100644 index 0000000..a460433 --- /dev/null +++ b/src/typings/dockerode.ts @@ -0,0 +1,162 @@ +interface DockerInfo { + ID: string; + Containers: number; + ContainersRunning: number; + ContainersPaused: number; + ContainersStopped: number; + Images: number; + Driver: string; + DriverStatus: [string, string][]; + DockerRootDir: string; + SystemStatus: [string, string][]; + Plugins: { + Volume: string[]; + Network: string[]; + Authorization: string[]; + Log: string[]; + }; + MemoryLimit: boolean; + SwapLimit: boolean; + KernelMemory: boolean; + CpuCfsPeriod: boolean; + CpuCfsQuota: boolean; + CPUShares: boolean; + CPUSet: boolean; + OomKillDisable: boolean; + IPv4Forwarding: boolean; + BridgeNfIptables: boolean; + BridgeNfIp6tables: boolean; + Debug: boolean; + NFd: number; + NGoroutines: number; + SystemTime: string; + LoggingDriver: string; + CgroupDriver: string; + NEventsListener: number; + KernelVersion: string; + OperatingSystem: string; + OSType: string; + Architecture: string; + NCPU: number; + MemTotal: number; + IndexServerAddress: string; + RegistryConfig: { + AllowNondistributableArtifactsCIDRs: string[]; + AllowNondistributableArtifactsHostnames: string[]; + InsecureRegistryCIDRs: string[]; + IndexConfigs: Record< + string, + { + Name: string; + Mirrors: string[]; + Secure: boolean; + Official: boolean; + } + >; + Mirrors: string[]; + }; + GenericResources: Array< + | { DiscreteResourceSpec: { Kind: string; Value: number } } + | { NamedResourceSpec: { Kind: string; Value: string } } + >; + HttpProxy: string; + HttpsProxy: string; + NoProxy: string; + Name: string; + Labels: string[]; + ExperimentalBuild: boolean; + ServerVersion: string; + ClusterStore: string; + ClusterAdvertise: string; + Runtimes: Record< + string, + { + path: string; + runtimeArgs?: string[]; + } + >; + DefaultRuntime: string; + Swarm: { + NodeID: string; + NodeAddr: string; + LocalNodeState: string; + ControlAvailable: boolean; + Error: string; + RemoteManagers: Array<{ + NodeID: string; + Addr: string; + }>; + Nodes: number; + Managers: number; + Cluster: { + ID: string; + Version: { + Index: number; + }; + CreatedAt: string; + UpdatedAt: string; + Spec: { + Name: string; + Labels: Record; + Orchestration: { + TaskHistoryRetentionLimit: number; + }; + Raft: { + SnapshotInterval: number; + KeepOldSnapshots: number; + LogEntriesForSlowFollowers: number; + ElectionTick: number; + HeartbeatTick: number; + }; + Dispatcher: { + HeartbeatPeriod: number; + }; + CAConfig: { + NodeCertExpiry: number; + ExternalCAs: Array<{ + Protocol: string; + URL: string; + Options: Record; + CACert: string; + }>; + SigningCACert: string; + SigningCAKey: string; + ForceRotate: number; + }; + EncryptionConfig: { + AutoLockManagers: boolean; + }; + TaskDefaults: { + LogDriver: { + Name: string; + Options: Record; + }; + }; + }; + TLSInfo: { + TrustRoot: string; + CertIssuerSubject: string; + CertIssuerPublicKey: string; + }; + RootRotationInProgress: boolean; + }; + }; + LiveRestoreEnabled: boolean; + Isolation: string; + InitBinary: string; + ContainerdCommit: { + ID: string; + Expected: string; + }; + RuncCommit: { + ID: string; + Expected: string; + }; + InitCommit: { + ID: string; + Expected: string; + }; + SecurityOptions: string[]; +} + +export type { DockerInfo }; diff --git a/src/typings/plugin.ts b/src/typings/plugin.ts new file mode 100644 index 0000000..9994ea6 --- /dev/null +++ b/src/typings/plugin.ts @@ -0,0 +1,25 @@ +import { ContainerInfo } from "~/typings/docker"; +import { HostStats } from "~/typings/docker"; + +interface Plugin { + name: string; + + // Container lifecycle hooks + onContainerStart?: (containerInfo: ContainerInfo) => void; + onContainerStop?: (containerInfo: ContainerInfo) => void; + onContainerExit?: (containerInfo: ContainerInfo) => void; + onContainerCreate?: (containerInfo: ContainerInfo) => void; + onContainerDestroy?: (containerInfo: ContainerInfo) => void; + onContainerPause?: (containerInfo: ContainerInfo) => void; + onContainerUnpause?: (containerInfo: ContainerInfo) => void; + onContainerRestart?: (containerInfo: ContainerInfo) => void; + onContainerUpdate?: (containerInfo: ContainerInfo) => void; + onContainerRename?: (containerInfo: ContainerInfo) => void; + onContainerHealthStatus?: (containerInfo: ContainerInfo) => void; + + // Host lifecycle hooks + onHostUnreachable?: (HostStats: HostStats) => void; + onHostReachableAgain?: (HostStats: HostStats) => void; +} + +export type { Plugin }; From ea8da8fb7dc60f10f27778ab69242122c70f7893 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 4 Mar 2025 22:34:03 +0100 Subject: [PATCH 143/324] Feat: Better logging usability, TASK level logs, database time needed in logs and Error Page --- bun.lock | 67 ++++++++++++++++--- package.json | 11 ++-- public/404.html | 110 ++++++++++++++++++++++++++++++++ public/DockStat.png | Bin 0 -> 79885 bytes src/core/database/repository.ts | 91 ++++++++++++++++++++++---- src/core/utils/logger.ts | 79 ++++++++++++++++++----- src/index.ts | 17 ++++- 7 files changed, 329 insertions(+), 46 deletions(-) create mode 100644 public/404.html create mode 100644 public/DockStat.png diff --git a/bun.lock b/bun.lock index 2c9571a..c03c8c9 100644 --- a/bun.lock +++ b/bun.lock @@ -4,9 +4,8 @@ "": { "name": "dockstatapi", "dependencies": { + "@elysiajs/static": "^1.2.0", "@elysiajs/swagger": "^1.2.2", - "@types/dockerode": "^3.3.34", - "@types/split2": "^4.2.3", "chalk": "^5.4.1", "dockerode": "^4.0.4", "elysia": "latest", @@ -15,7 +14,11 @@ "winston-transport": "^4.9.0", }, "devDependencies": { + "@types/dockerode": "^3.3.34", + "@types/split2": "^4.2.3", "bun-types": "latest", + "cross-env": "^7.0.3", + "wrap-ansi": "^9.0.0", }, }, }, @@ -29,6 +32,8 @@ "@dabh/diagnostics": ["@dabh/diagnostics@2.0.3", "", { "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA=="], + "@elysiajs/static": ["@elysiajs/static@1.2.0", "", { "dependencies": { "node-cache": "^5.1.2" }, "peerDependencies": { "elysia": ">= 1.2.0" } }, "sha512-oLpAi8c+maPpA0XhhK3BELaIjIG+nXg/K9p8cFfW4q5ayRD59a3MOMOOGgpiXZkHJzLPWcouhhyyLAYtaANW4g=="], + "@elysiajs/swagger": ["@elysiajs/swagger@1.2.2", "", { "dependencies": { "@scalar/themes": "^0.9.52", "@scalar/types": "^0.0.12", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.2.0" } }, "sha512-DG0PbX/wzQNQ6kIpFFPCvmkkWTIbNWDS7lVLv3Puy6ONklF14B4NnbDfpYjX1hdSYKeCqKBBOuenh6jKm8tbYA=="], "@grpc/grpc-js": ["@grpc/grpc-js@1.12.6", "", { "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-JXUj6PI0oqqzTGvKtzOkxtpsyPRNsrmhh41TtIz/zEB6J+AUiZZ0dxWzcMwO9Ns5rmSPuMdghlTbUuqIM48d3Q=="], @@ -81,9 +86,9 @@ "@unhead/schema": ["@unhead/schema@1.11.19", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-7VhYHWK7xHgljdv+C01MepCSYZO2v6OhgsfKWPxRQBDDGfUKCUaChox0XMq3tFvXP6u4zSp6yzcDw2yxCfVMwg=="], - "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], - "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], "asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="], @@ -107,6 +112,8 @@ "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + "clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="], + "color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="], "color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], @@ -121,6 +128,10 @@ "cpu-features": ["cpu-features@0.0.10", "", { "dependencies": { "buildcheck": "~0.0.6", "nan": "^2.19.0" } }, "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA=="], + "cross-env": ["cross-env@7.0.3", "", { "dependencies": { "cross-spawn": "^7.0.1" }, "bin": { "cross-env": "src/bin/cross-env.js", "cross-env-shell": "src/bin/cross-env-shell.js" } }, "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], "docker-modem": ["docker-modem@5.0.6", "", { "dependencies": { "debug": "^4.1.1", "readable-stream": "^3.5.0", "split-ca": "^1.0.1", "ssh2": "^1.15.0" } }, "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ=="], @@ -129,7 +140,7 @@ "elysia": ["elysia@1.2.21", "", { "dependencies": { "@sinclair/typebox": "^0.34.27", "cookie": "^1.0.2", "memoirist": "^0.3.0", "openapi-types": "^12.1.3" }, "peerDependencies": { "typescript": ">= 5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-E9b1JcB7fiQ2ptk24W8OnBrMYUoKzffIXob9uTVUKhqOKxaXAd9UyWBeyr7JCDa/VD/b/9S8aIey9/YJsK5sLg=="], - "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], "enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="], @@ -145,6 +156,8 @@ "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + "get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="], + "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], @@ -157,6 +170,8 @@ "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="], "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], @@ -173,12 +188,16 @@ "nan": ["nan@2.22.1", "", {}, "sha512-pfRR4ZcNTSm2ZFHaztuvbICf+hyiG6ecA06SfAxoPmuHjvMu0KUIae7Y8GyVkbBqeEIidsmXeYooWIX9+qjfRQ=="], + "node-cache": ["node-cache@5.1.2", "", { "dependencies": { "clone": "2.x" } }, "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg=="], + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], "one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="], "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], "protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], @@ -195,6 +214,10 @@ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], "split-ca": ["split-ca@1.0.1", "", {}, "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ=="], @@ -205,11 +228,11 @@ "stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="], - "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], - "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], "tar-fs": ["tar-fs@2.0.1", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.0.0" } }, "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA=="], @@ -221,17 +244,21 @@ "tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="], + "typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], + "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], "uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "winston": ["winston@3.17.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw=="], "winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="], - "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="], "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], @@ -247,12 +274,32 @@ "@types/ssh2/@types/node": ["@types/node@18.19.76", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-yvR7Q9LdPz2vGpmpJX5LolrgRdWvB67MJKDPSgIIzpFbaf9a1j/f5DnLp5VDyHGMR0QZHlTr1afsD87QCXFHKw=="], - "ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.1.8", "", {}, "sha512-iufA5/6hPCmRIVD2eh7qGpoKvoA08Gw/qUb2JECifBtAwA93fo7+1k9uHK440f2LMJsbxIzA+nv7RS0BmfiO/g=="], "@types/ssh2/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], - "ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "cliui/wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "cliui/wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], } } diff --git a/package.json b/package.json index 515c301..c6f6f74 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,10 @@ "version": "2.1.0", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "dev": "docker compose -f ./docker/docker-compose.dev.yaml up -d && bun run --watch src/index.ts" + "dev": "docker compose -f docker/docker-compose.dev.yaml up -d && cross-env NODE_ENV=dev bun run --watch src/index.ts" }, "dependencies": { + "@elysiajs/static": "^1.2.0", "@elysiajs/swagger": "^1.2.2", "chalk": "^5.4.1", "dockerode": "^4.0.4", @@ -15,12 +16,14 @@ "winston-transport": "^4.9.0" }, "devDependencies": { - "bun-types": "latest", "@types/dockerode": "^3.3.34", - "@types/split2": "^4.2.3" + "@types/split2": "^4.2.3", + "bun-types": "latest", + "cross-env": "^7.0.3", + "wrap-ansi": "^9.0.0" }, "module": "src/index.js", "trustedDependencies": [ "protobufjs" ] -} +} \ No newline at end of file diff --git a/public/404.html b/public/404.html new file mode 100644 index 0000000..a0169cf --- /dev/null +++ b/public/404.html @@ -0,0 +1,110 @@ + + + + + + + 404 - Page Not Found + + + + +
+ +
404
+
+ Oops! The page you're looking for doesn't exist. +
+
+ + +
+
+ + + \ No newline at end of file diff --git a/public/DockStat.png b/public/DockStat.png new file mode 100644 index 0000000000000000000000000000000000000000..d375bd49107c79a960488d6062276a72cf6bd512 GIT binary patch literal 79885 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⪼EggaS+_T%ej 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 KU+70|um2a&GMD85 literal 0 HcmV?d00001 diff --git a/src/core/database/repository.ts b/src/core/database/repository.ts index 7c60a5d..bb43497 100644 --- a/src/core/database/repository.ts +++ b/src/core/database/repository.ts @@ -7,6 +7,8 @@ const db = new Database("dockstatapi.db"); export const dbFunctions = { init() { + const startTime = Date.now(); + logger.debug("__task__ __db__ Initializing Database ⏳") db.exec(` CREATE TABLE IF NOT EXISTS docker_hosts ( name TEXT, @@ -88,9 +90,13 @@ export const dbFunctions = { ); stmt.run("Localhost", "localhost:2375", false); } + const duration = Date.now() - startTime; + logger.debug(`__task__ __db__ Initializing Database ✔️ (${duration}ms)`); }, addDockerHost(hostId: string, url: string, secure: boolean) { + const startTime = Date.now(); + logger.debug("__task__ __db__ Adding Docker Host ⏳") if ( typeof hostId !== "string" || typeof url !== "string" || @@ -104,16 +110,23 @@ export const dbFunctions = { INSERT INTO docker_hosts (name, url, secure) VALUES (?, ?, ?) `); - return stmt.run(hostId, url, secure); + const data = stmt.run(hostId, url, secure); + const duration = Date.now() - startTime; + logger.debug(`__task__ __db__ Adding Docker Host ✔️ (${duration}ms)`); + return data }, getDockerHosts(): DockerHost[] { + const startTime = Date.now(); + logger.debug("__task__ __db__ Getting Docker Host ⏳") const stmt = db.prepare(` SELECT name, url, secure FROM docker_hosts ORDER BY name DESC `); const data = stmt.all(); + const duration = Date.now() - startTime; + logger.debug(`__task__ __db__ Getting Docker Host ✔️ (${duration}ms)`); return data as DockerHost[]; }, @@ -141,15 +154,22 @@ export const dbFunctions = { }, getAllLogs() { + const startTime = Date.now(); + logger.debug("__task__ __db__ Getting all Logs ⏳") const stmt = db.prepare(` SELECT timestamp, level, message, file, line FROM backend_log_entries ORDER BY timestamp DESC `); - return stmt.all(); + const data = stmt.all(); + const duration = Date.now() - startTime; + logger.debug(`__task__ __db__ Getting all Logs ✔️ (${duration}ms)`); + return data }, getLogsByLevel(level: string) { + const startTime = Date.now(); + logger.debug("__task__ __db__ Getting level-logs ⏳") if (typeof level !== "string") { logger.crit("Level parameter must be a string"); throw new TypeError("Level parameter must be a string"); @@ -161,10 +181,15 @@ export const dbFunctions = { WHERE level = ? ORDER BY timestamp DESC `); - return stmt.all(level); + const data = stmt.all(level); + const duration = Date.now() - startTime; + logger.debug(`__task__ __db__ Getting level-logs ✔️ (${duration}ms)`); + return data }, updateDockerHost(name: string, url: string, secure: boolean) { + const startTime = Date.now(); + logger.debug("__task__ __db__ Updating Docker Host ⏳") if ( typeof name !== "string" || typeof url !== "string" || @@ -179,10 +204,15 @@ export const dbFunctions = { SET url = ?, secure = ? WHERE name = ? `); - return stmt.run(url, secure, name); + const data = stmt.run(url, secure, name); + const duration = Date.now() - startTime; + logger.debug(`__task__ __db__ Updating Docker Host ✔️ (${duration}ms)`); + return data }, deleteDockerHost(name: string) { + const startTime = Date.now(); + logger.debug("__task__ __db__ Deleting Docker Host ⏳") if (typeof name !== "string") { logger.crit("Invalid parameter type for deleteDockerHost"); throw new TypeError("Name parameter must be a string"); @@ -192,17 +222,27 @@ export const dbFunctions = { DELETE FROM docker_hosts WHERE name = ? `); - return stmt.run(name); + const data = stmt.run(name); + const duration = Date.now() - startTime; + logger.debug(`__task__ __db__ Deleting Docker Host ✔️ (${duration}ms)`); + return data }, clearAllLogs() { + const startTime = Date.now(); + logger.debug("__task__ __db__ Clearing all Logs ⏳") const stmt = db.prepare(` DELETE FROM backend_log_entries `); - return stmt.run(); + const data = stmt.run(); + const duration = Date.now() - startTime; + logger.debug(`__task__ __db__ Clearing all Logs ✔️ (${duration}ms)`); + return data }, clearLogsByLevel(level: string) { + const startTime = Date.now(); + logger.debug("__task__ __db__ Clearing all logs by level ⏳") if (typeof level !== "string") { logger.crit("Invalid parameter type for clearLogsByLevel"); throw new TypeError("Level parameter must be a string"); @@ -212,7 +252,10 @@ export const dbFunctions = { DELETE FROM backend_log_entries WHERE level = ? `); - return stmt.run(level); + const data = stmt.run(level); + const duration = Date.now() - startTime; + logger.debug(`__task__ __db__ Clearing all logs by level ✔️ (${duration}ms)`); + return data }, updateConfig( @@ -220,6 +263,8 @@ export const dbFunctions = { fetching_interval: number, keep_data_for: number, ) { + const startTime = Date.now(); + logger.debug("__task__ __db__ Updating config ⏳") if ( typeof polling_rate !== "number" || typeof fetching_interval !== "number" || @@ -236,16 +281,24 @@ export const dbFunctions = { keep_data_for = ? `); - return stmt.run(polling_rate, fetching_interval, keep_data_for); + const data = stmt.run(polling_rate, fetching_interval, keep_data_for); + const duration = Date.now() - startTime; + logger.debug(`__task__ __db__ Updating config ✔️ (${duration}ms)`); + return data }, getConfig() { + const startTime = Date.now(); + logger.debug("__task__ __db__ Getting config ⏳") const stmt = db.prepare(` SELECT polling_rate, keep_data_for, fetching_interval FROM config `); - return stmt.all(); + const data = stmt.all(); + const duration = Date.now() - startTime; + logger.debug(`__task__ __db__ Getting config ✔️ (${duration}ms)`); + return data }, // Stats: @@ -259,6 +312,8 @@ export const dbFunctions = { cpu_usage: number, memory_usage: number, ) { + const startTime = Date.now(); + logger.debug("__task__ __db__ Adding container statistics ⏳") if ( typeof id !== "string" || typeof hostId !== "string" || @@ -277,7 +332,7 @@ export const dbFunctions = { INSERT INTO container_stats (id, hostId, name, image, status, state, cpu_usage, memory_usage) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `); - return stmt.run( + const data = stmt.run( id, hostId, name, @@ -287,9 +342,14 @@ export const dbFunctions = { cpu_usage, memory_usage, ); + const duration = Date.now() - startTime; + logger.debug(`__task__ __db__ Adding container statistics ✔️ (${duration}ms)`); + return data }, deleteOldData(days: number) { + const startTime = Date.now(); + logger.debug("__task__ __db__ Deleting old data ⏳") if (typeof days !== "number") { logger.crit("Invalid parameter type for deleteOldData"); throw new TypeError("Days parameter must be a number"); @@ -306,9 +366,13 @@ export const dbFunctions = { WHERE timestamp < datetime('now', '-' || ? || ' days') `); deleteLogsStmt.run(days); + const duration = Date.now() - startTime; + logger.debug(`__task__ __db__ Deleting old data ✔️ (${duration}ms)`); }, updateHostStats(stats: HostStats) { + const startTime = Date.now(); + logger.debug("__task__ __db__ Update Host Stats ⏳") const labelsJson = JSON.stringify(stats.labels); const stmt = db.prepare(` INSERT INTO host_stats ( @@ -341,7 +405,7 @@ export const dbFunctions = { containersPaused = excluded.containersPaused, images = excluded.images; `); - return stmt.run( + const data = stmt.run( stats.hostId, stats.dockerVersion, stats.apiVersion, @@ -356,7 +420,8 @@ export const dbFunctions = { stats.containersPaused, stats.images, ); + const duration = Date.now() - startTime; + logger.debug(`__task__ __db__ Update Host stats ✔️ (${duration}ms)`); + return data }, }; - -dbFunctions.init(); diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts index a173cb7..013de16 100644 --- a/src/core/utils/logger.ts +++ b/src/core/utils/logger.ts @@ -2,6 +2,10 @@ import { createLogger, format, transports } from "winston"; import path from "path"; import chalk, { ChalkInstance } from "chalk"; import { dbFunctions } from "../database/repository"; +import wrapAnsi from "wrap-ansi"; + +// Change to false here if dont want the spacing on a wrapped line +const padNewlines: boolean = true; const fileLineFormat = format((info) => { try { @@ -9,59 +13,100 @@ const fileLineFormat = format((info) => { if (stack) { for (let i = 2; i < stack.length; i++) { const line = stack[i].trim(); - if ( - !line.includes("node_modules") && - !line.includes(path.basename(__filename)) - ) { + // Exclude lines from node_modules or the current file + if (!line.includes("node_modules") && !line.includes(path.basename(__filename))) { const matches = line.match(/\(?(.+):(\d+):(\d+)\)?$/); if (matches) { info.file = path.basename(matches[1]); - info.line = parseInt(matches[2]); + info.line = parseInt(matches[2], 10); break; } } } } } catch (err) { - // Ignore errors in case stack trace parsing fails + // Ignore errors during stack trace extraction } return info; }); +const formatTerminalMessage = (message: string, prefixLength: number) => { + const maxWidth = process.stdout.columns || 80; + const wrapWidth = maxWidth - prefixLength - 15; + + if (padNewlines) { + const wrapped = wrapAnsi(chalk.gray(message), wrapWidth, { + trim: true, + hard: true, + }); + + return wrapped + .split("\n") + .map((line, i) => (i === 0 ? line : " ".repeat(prefixLength) + line)) + .join("\n"); + } + return message; +}; + export const logger = createLogger({ level: "debug", format: format.combine( format.timestamp({ format: "DD/MM HH:mm:ss" }), fileLineFormat(), format.printf(({ timestamp, level, message, file, line }) => { - const levelColors: { [key: string]: ChalkInstance } = { + const levelColors: Record = { error: chalk.red.bold, warn: chalk.yellow.bold, info: chalk.green.bold, debug: chalk.blue.bold, verbose: chalk.cyan.bold, silly: chalk.magenta.bold, + task: chalk.cyan.bold }; + if ((message as string).startsWith("__task__")) { + message = (message as string).replaceAll("__task__", "").trimStart(); + level = "task" + if ((message as string).startsWith("__db__")) { + message = (message as string).replaceAll("__db__", "").trimStart(); + message = `${chalk.magenta("DB")} ${message}` + } + } + const paddedLevel = level.toUpperCase().padEnd(5); const coloredLevel = (levelColors[level] || chalk.white)(paddedLevel); - const coloredContext = chalk.cyan(`${file}:${line}`); - const coloredMessage = chalk.gray(message); - const coloredTimestamp = chalk.yellow(`${timestamp}`); + const coloredContext = chalk.cyan(`${file as string}:${line as number}`); + const coloredTimestamp = chalk.yellow(timestamp); + const ansiRegex = /\x1B\[[0-?9;]*[mG]/g; try { dbFunctions.addLogEntry( - level, - message as string, - file as string, - line as number, + (level as string).replace(ansiRegex, ''), + (message as string).replace(ansiRegex, ''), + (file as string).replace(ansiRegex, ''), + line as number ); } catch (error) { - logger.error(`Error inserting log into DB: ${error as string}`); + // Use console.error to avoid recursive logging + console.error(`Error inserting log into DB: ${String(error)}`); + console.error("Aborting due to risk of recursion!") + process.abort() + } + + if (process.env.NODE_ENV !== "dev") { + return `${coloredLevel} [ ${coloredTimestamp} ] - ${chalk.gray( + message + )} - [ ${coloredContext} ]`; } - return `${coloredLevel} [ ${coloredTimestamp} ] - ${coloredMessage} - [ ${coloredContext} ]`; - }), + const prefix = `${paddedLevel} [ ${timestamp} ] - `; + const prefixLength = prefix.length; + const formattedMessage = formatTerminalMessage( + message as string, + prefixLength + ); + return `${coloredLevel} [ ${coloredTimestamp} ] - ${formattedMessage} - [ ${coloredContext} ]`; + }) ), transports: [new transports.Console()], }); diff --git a/src/index.ts b/src/index.ts index 90e7367..8758644 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,12 +9,15 @@ import { backendLogs } from "~/routes/logs"; import { dockerWebsocketRoutes } from "~/routes/docker-websocket"; import { apiConfigRoutes } from "~/routes/api-config"; import { setSchedules } from "~/core/docker/scheduler"; +import staticPlugin from "@elysiajs/static"; +console.log("") logger.info("Starting server..."); dbFunctions.init(); const DockStatAPI = new Elysia() + .use(staticPlugin()) .use( swagger({ documentation: { @@ -46,12 +49,20 @@ const DockStatAPI = new Elysia() .use(backendLogs) .use(dockerWebsocketRoutes) .use(apiConfigRoutes) - .get("/health", () => ({ status: "healthy" }), { tags: ["Utils"] }); + .get("/health", () => ({ status: "healthy" }), { tags: ["Utils"] }) + .onError(({ code, set }) => { + if (code === 'NOT_FOUND') { + logger.warn("Unknown route, showing error page!") + set.status = 404 + set.headers['Content-Type'] = 'text/html' + return Bun.file('public/404.html') + } + }); async function startServer() { try { - await loadPlugins("./src/plugins"); + await loadPlugins("./src/plugins"); DockStatAPI.listen(3000, ({ hostname, port }) => { logger.info(`DockStatAPI is running at http://${hostname}:${port}`); logger.info( @@ -66,4 +77,6 @@ async function startServer() { await startServer(); await setSchedules(); + logger.info("Started server"); +console.log("") From 68ec16804a3d4013ef35bd14f214174d91f75558 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 4 Mar 2025 23:11:25 +0100 Subject: [PATCH 144/324] Feat: Adjustable log level and minor changes --- README.md | 3 +++ package.json | 6 ++++-- src/core/utils/logger.ts | 2 +- src/index.ts | 4 ++-- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index eb71a1e..656260b 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,9 @@ The DockStat API provides the following endpoints: - `DELETE /logs`: Clear all backend logs. - `DELETE /logs/:level`: Clear logs by log level. +### Webocket +- `WS(S) /docker/stats`: Retrieve the current API configuration. + ## API The DockStat API exposes the following endpoints: diff --git a/package.json b/package.json index c6f6f74..d8460bd 100644 --- a/package.json +++ b/package.json @@ -2,8 +2,10 @@ "name": "dockstatapi", "version": "2.1.0", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "dev": "docker compose -f docker/docker-compose.dev.yaml up -d && cross-env NODE_ENV=dev bun run --watch src/index.ts" + "start": "cross-env NODE_ENV=production LOG_LEVEL=info bun run src/index.ts", + "start:linux": "NODE_ENV=production LOG_LEVEL=info bun run src/index.ts", + "dev": "docker compose -f docker/docker-compose.dev.yaml up -d && cross-env NODE_ENV=dev bun run --watch src/index.ts", + "build": "bun build --target bun src/index.ts --outdir ./dist" }, "dependencies": { "@elysiajs/static": "^1.2.0", diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts index 013de16..82dc789 100644 --- a/src/core/utils/logger.ts +++ b/src/core/utils/logger.ts @@ -49,7 +49,7 @@ const formatTerminalMessage = (message: string, prefixLength: number) => { }; export const logger = createLogger({ - level: "debug", + level: process.env.LOG_LEVEL || 'debug', format: format.combine( format.timestamp({ format: "DD/MM HH:mm:ss" }), fileLineFormat(), diff --git a/src/index.ts b/src/index.ts index 8758644..3a4521f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -61,9 +61,9 @@ const DockStatAPI = new Elysia() async function startServer() { try { - await loadPlugins("./src/plugins"); DockStatAPI.listen(3000, ({ hostname, port }) => { + console.log("") logger.info(`DockStatAPI is running at http://${hostname}:${port}`); logger.info( `Swagger API Documentation available at http://${hostname}:${port}/swagger`, @@ -75,8 +75,8 @@ async function startServer() { } } -await startServer(); await setSchedules(); +await startServer(); logger.info("Started server"); console.log("") From 4f2be554924512e1a70b53a0993a10856b208906 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sat, 8 Mar 2025 16:48:19 +0100 Subject: [PATCH 145/324] Feat: Package info, contributers, License and README update --- LICENSE | 407 +++++++++++++++++++++++++++++++++ README.md | 3 +- package.json | 10 +- src/core/utils/package-json.ts | 23 ++ src/routes/api-config.ts | 37 ++- tsconfig.json | 2 +- 6 files changed, 478 insertions(+), 4 deletions(-) create mode 100644 LICENSE create mode 100644 src/core/utils/package-json.ts diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..428e595 --- /dev/null +++ b/LICENSE @@ -0,0 +1,407 @@ +Attribution-NonCommercial 4.0 International + +======================================================================= + +Creative Commons Corporation ("Creative Commons") is not a law firm and +does not provide legal services or legal advice. Distribution of +Creative Commons public licenses does not create a lawyer-client or +other relationship. Creative Commons makes its licenses and related +information available on an "as-is" basis. Creative Commons gives no +warranties regarding its licenses, any material licensed under their +terms and conditions, or any related information. Creative Commons +disclaims all liability for damages resulting from their use to the +fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and +conditions that creators and other rights holders may use to share +original works of authorship and other material subject to copyright +and certain other rights specified in the public license below. The +following considerations are for informational purposes only, are not +exhaustive, and do not form part of our licenses. + + Considerations for licensors: Our public licenses are + intended for use by those authorized to give the public + permission to use material in ways otherwise restricted by + copyright and certain other rights. Our licenses are + irrevocable. Licensors should read and understand the terms + and conditions of the license they choose before applying it. + Licensors should also secure all rights necessary before + applying our licenses so that the public can reuse the + material as expected. Licensors should clearly mark any + material not subject to the license. This includes other CC- + licensed material, or material used under an exception or + limitation to copyright. More considerations for licensors: + wiki.creativecommons.org/Considerations_for_licensors + + Considerations for the public: By using one of our public + licenses, a licensor grants the public permission to use the + licensed material under specified terms and conditions. If + the licensor's permission is not necessary for any reason--for + example, because of any applicable exception or limitation to + copyright--then that use is not regulated by the license. Our + licenses grant only permissions under copyright and certain + other rights that a licensor has authority to grant. Use of + the licensed material may still be restricted for other + reasons, including because others have copyright or other + rights in the material. A licensor may make special requests, + such as asking that all changes be marked or described. + Although not required by our licenses, you are encouraged to + respect those requests where reasonable. More considerations + for the public: + wiki.creativecommons.org/Considerations_for_licensees + +======================================================================= + +Creative Commons Attribution-NonCommercial 4.0 International Public +License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution-NonCommercial 4.0 International Public License ("Public +License"). To the extent this Public License may be interpreted as a +contract, You are granted the Licensed Rights in consideration of Your +acceptance of these terms and conditions, and the Licensor grants You +such rights in consideration of benefits the Licensor receives from +making the Licensed Material available under these terms and +conditions. + + +Section 1 -- Definitions. + + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright + and Similar Rights in Your contributions to Adapted Material in + accordance with the terms and conditions of this Public License. + + c. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + d. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + + e. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + + f. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + + g. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + + h. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + + i. NonCommercial means not primarily intended for or directed towards + commercial advantage or monetary compensation. For purposes of + this Public License, the exchange of the Licensed Material for + other material subject to Copyright and Similar Rights by digital + file-sharing or similar means is NonCommercial provided there is + no payment of monetary compensation in connection with the + exchange. + + j. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + + k. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + + l. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part, for NonCommercial purposes only; and + + b. produce, reproduce, and Share Adapted Material for + NonCommercial purposes only. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties, including when + the Licensed Material is used other than for NonCommercial + purposes. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified + form), You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + 4. If You Share Adapted Material You produce, the Adapter's + License You apply must not prevent recipients of the Adapted + Material from complying with this Public License. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database for NonCommercial purposes + only; + + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material; and + + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + + +Section 6 -- Term and Termination. + + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + + +Section 7 -- Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. + +======================================================================= + +Creative Commons is not a party to its public +licenses. Notwithstanding, Creative Commons may elect to apply one of +its public licenses to material it publishes and in those instances +will be considered the "Licensor." The text of the Creative Commons +public licenses is dedicated to the public domain under the CC0 Public +Domain Dedication. Except for the limited purpose of indicating that +material is shared under a Creative Commons public license or as +otherwise permitted by the Creative Commons policies published at +creativecommons.org/policies, Creative Commons does not authorize the +use of the trademark "Creative Commons" or any other trademark or logo +of Creative Commons without its prior written consent including, +without limitation, in connection with any unauthorized modifications +to any of its public licenses or any other arrangements, +understandings, or agreements concerning use of licensed material. For +the avoidance of doubt, this paragraph does not form part of the +public licenses. + +Creative Commons may be contacted at creativecommons.org. diff --git a/README.md b/README.md index 656260b..9784f98 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,8 @@ The DockStat API exposes the following endpoints: ## License -This project is licensed under the [MIT License](LICENSE). +This project is licensed under the CC BY-NC 4.0 License. +[cc-by-nc-image]: https://licensebuttons.net/l/by-nc/4.0/88x31.png ## Testing diff --git a/package.json b/package.json index d8460bd..d28b22a 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,13 @@ { "name": "dockstatapi", + "author": { + "email": "info@itsnik.de", + "name": "ItsNik", + "url": "https://github.com/Its4Nik" + }, + "license": "CC BY-NC 4.0", + "contributors": [], + "description": "DockStatAPI is an API backend featuring plugins and more for DockStat", "version": "2.1.0", "scripts": { "start": "cross-env NODE_ENV=production LOG_LEVEL=info bun run src/index.ts", @@ -28,4 +36,4 @@ "trustedDependencies": [ "protobufjs" ] -} \ No newline at end of file +} diff --git a/src/core/utils/package-json.ts b/src/core/utils/package-json.ts new file mode 100644 index 0000000..5b84757 --- /dev/null +++ b/src/core/utils/package-json.ts @@ -0,0 +1,23 @@ +import packageJson from "~/../package.json"; + +const version = packageJson.version; +const description = packageJson.description; +const authorName = packageJson.author.name; +const authorEmail = packageJson.author.email; +const authorWebsite = packageJson.author.url; +const license = packageJson.license; +const contributers = packageJson.contributors; +const dependencies = packageJson.dependencies; +const devDependencies = packageJson.devDependencies; + +export { + version, + description, + authorName, + authorEmail, + authorWebsite, + license, + contributers, + dependencies, + devDependencies, +}; diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index a77d2b4..b57fbf7 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -3,6 +3,18 @@ import { dbFunctions } from "~/core/database/repository"; import { logger } from "~/core/utils/logger"; import { responseHandler } from "~/core/utils/respone-handler"; import { config } from "~/typings/database"; +import { + version, + authorEmail, + authorName, + authorWebsite, + contributers, + dependencies, + description, + devDependencies, + license, +} from "~/core/utils/package-json"; +import { describe } from "test"; export const apiConfigRoutes = new Elysia({ prefix: "/config" }) .get( @@ -55,4 +67,27 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) }), tags: ["Management"], }, - ); + ) + .get("/package", async ({ set }) => { + try { + logger.debug("Fetching package.json"); + const data = { + version: version, + description: description, + license: license, + authorName: authorName, + authorEmail: authorEmail, + authorWebsite: authorWebsite, + contributers: contributers, + dependencies: dependencies, + devDependencies: devDependencies, + }; + return data; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Error while reading package.json", + ); + } + }); diff --git a/tsconfig.json b/tsconfig.json index b95e7e0..ab566ad 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -39,7 +39,7 @@ ] /* Specify type package names to be included without being referenced in a source file. */, // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ - // "resolveJsonModule": true, /* Enable importing .json files. */ + "resolveJsonModule": true /* Enable importing .json files. */, // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ /* JavaScript Support */ From d1566b16f3e76e7494c438e92c427efc9f0d4494 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 11 Mar 2025 18:38:04 +0100 Subject: [PATCH 146/324] Update README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 2577866..f1be46f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +# Deprecation Warning! +# V2 is abondend, since there whhere to many issues in the codebase! +# Please see v3 (next commit from this one onwards, all other branches which are not based on bun will be deleted!) + # DockStatAPI v2 ![Dockstat Logo](.github/DockStat.png) From 0ed8ebdfc157d342d351e91bcfc9d2403fe49481 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 11 Mar 2025 18:38:26 +0100 Subject: [PATCH 147/324] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f1be46f..4fc979c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Deprecation Warning! -# V2 is abondend, since there whhere to many issues in the codebase! +# V2 is abondend, since there where to many issues in the codebase! # Please see v3 (next commit from this one onwards, all other branches which are not based on bun will be deleted!) # DockStatAPI v2 From 7d6fafe3ef8d6c5709b44e235d59c6e741aba0ef Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 11 Mar 2025 22:14:51 +0100 Subject: [PATCH 148/324] Feat: Stacks, RealyController (WIP) --- .dockerignore | 17 +- .gitignore | 48 +--- .local-tests/stacks.md | 39 +++ bun.lock | 6 + package.json | 12 +- public/404.html | 15 +- src/core/database/repository.ts | 187 ++++++++++--- src/core/docker/client.ts | 17 ++ src/core/docker/relay-controller.ts | 13 + src/index.ts | 13 +- src/routes/api-config.ts | 7 +- src/routes/stacks.ts | 245 +++++++++++++++++ src/typings/database.ts | 12 +- src/typings/docker-compose.ts | 393 ++++++++++++++++++++++++++++ 14 files changed, 907 insertions(+), 117 deletions(-) create mode 100644 .local-tests/stacks.md create mode 100644 src/core/docker/relay-controller.ts create mode 100644 src/routes/stacks.ts create mode 100644 src/typings/docker-compose.ts diff --git a/.dockerignore b/.dockerignore index f965aed..152a619 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,15 +1,4 @@ +*.db* +stacks node_modules -Dockerfile* -docker-compose* -.dockerignore -.git -.gitignore -README.md -LICENSE -.vscode -Makefile -helm-charts -.env -.editorconfig -.idea -coverage* +*.md \ No newline at end of file diff --git a/.gitignore b/.gitignore index 138ece6..964a918 100644 --- a/.gitignore +++ b/.gitignore @@ -1,45 +1,3 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -*.db -*.db-journal - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env.local -.env.development.local -.env.test.local -.env.production.local - -# vercel -.vercel - -**/*.trace -**/*.zip -**/*.tar.gz -**/*.tgz -**/*.log -package-lock.json -**/*.bun +*.db* +stacks +node_modules \ No newline at end of file diff --git a/.local-tests/stacks.md b/.local-tests/stacks.md new file mode 100644 index 0000000..4d9290c --- /dev/null +++ b/.local-tests/stacks.md @@ -0,0 +1,39 @@ +# Testing Stacks + +## Deployment + +### Values + +- compose_spec +- name +- version +- automatic_reboot_on_error +- isCustom +- image_updates +- source +- stack_prefix + +### JSON +```json +{ + "compose_spec": { + "name": "Local Test", + "services": { + "nginx": { + "container_name": "Local-test-nginx", + "image": "dockerbogo/docker-nginx-hello-world", + "ports": [ + "8081:80" + ] + } + } + }, + "name": "Local-Test", + "version": 1, + "automatic_reboot_on_error": true, + "isCustom": true, + "image_updates": true, + "source": "Local", + "stack_prefix": "" +} +``` diff --git a/bun.lock b/bun.lock index c03c8c9..f91ae69 100644 --- a/bun.lock +++ b/bun.lock @@ -7,11 +7,13 @@ "@elysiajs/static": "^1.2.0", "@elysiajs/swagger": "^1.2.2", "chalk": "^5.4.1", + "docker-compose": "^1.1.1", "dockerode": "^4.0.4", "elysia": "latest", "split2": "^4.2.0", "winston": "^3.17.0", "winston-transport": "^4.9.0", + "yaml": "^2.7.0", }, "devDependencies": { "@types/dockerode": "^3.3.34", @@ -134,6 +136,8 @@ "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "docker-compose": ["docker-compose@1.1.1", "", { "dependencies": { "yaml": "^2.2.2" } }, "sha512-UkIUz0LtzuO17Ijm6SXMGtfZMs7IvbNwvuJBiBuN93PIhr/n9/sbJMqpvYFaCBGfwu1ZM4PPPDgQzeeke4lEoA=="], + "docker-modem": ["docker-modem@5.0.6", "", { "dependencies": { "debug": "^4.1.1", "readable-stream": "^3.5.0", "split-ca": "^1.0.1", "ssh2": "^1.15.0" } }, "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ=="], "dockerode": ["dockerode@4.0.4", "", { "dependencies": { "@balena/dockerignore": "^1.0.2", "@grpc/grpc-js": "^1.11.1", "@grpc/proto-loader": "^0.7.13", "docker-modem": "^5.0.6", "protobufjs": "^7.3.2", "tar-fs": "~2.0.1", "uuid": "^10.0.0" } }, "sha512-6GYP/EdzEY50HaOxTVTJ2p+mB5xDHTMJhS+UoGrVyS6VC+iQRh7kZ4FRpUYq6nziby7hPqWhOrFFUFTMUZJJ5w=="], @@ -264,6 +268,8 @@ "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + "yaml": ["yaml@2.7.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA=="], + "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], diff --git a/package.json b/package.json index d28b22a..0650947 100644 --- a/package.json +++ b/package.json @@ -13,17 +13,23 @@ "start": "cross-env NODE_ENV=production LOG_LEVEL=info bun run src/index.ts", "start:linux": "NODE_ENV=production LOG_LEVEL=info bun run src/index.ts", "dev": "docker compose -f docker/docker-compose.dev.yaml up -d && cross-env NODE_ENV=dev bun run --watch src/index.ts", - "build": "bun build --target bun src/index.ts --outdir ./dist" + "dev:clean": "bun dev ; echo '\nExiting...' ; bun clean", + "build": "bun build --target bun src/index.ts --outdir ./dist", + "clean": "bun run clean:win || bun run clean:lin", + "clean:win": "node -e \"process.exit(process.platform === 'win32' ? 0 : 1)\" && cmd /c del /Q dockstatapi.db* && echo 'success'", + "clean:lin": "node -e \"process.exit(process.platform !== 'win32' ? 0 : 1)\" && rm -f dockstatapi.db* && echo 'success'" }, "dependencies": { "@elysiajs/static": "^1.2.0", "@elysiajs/swagger": "^1.2.2", "chalk": "^5.4.1", + "docker-compose": "^1.1.1", "dockerode": "^4.0.4", "elysia": "latest", "split2": "^4.2.0", "winston": "^3.17.0", - "winston-transport": "^4.9.0" + "winston-transport": "^4.9.0", + "yaml": "^2.7.0" }, "devDependencies": { "@types/dockerode": "^3.3.34", @@ -36,4 +42,4 @@ "trustedDependencies": [ "protobufjs" ] -} +} \ No newline at end of file diff --git a/public/404.html b/public/404.html index a0169cf..39b107d 100644 --- a/public/404.html +++ b/public/404.html @@ -26,7 +26,6 @@ .logo { height: 15vh; margin-bottom: 1rem; - animation: bounce 4s infinite; } .error-code { @@ -66,27 +65,17 @@ transform: translateY(-2px); } - @keyframes bounce { - - 0%, - 100% { - transform: scale(1, 1); - } - - 50% { - transform: scale(1.1, 1.1); - } - } - @keyframes error { 0%, 100% { color: rgb(255, 255, 255); + transform: scale(1, 1); } 50% { color: rgb(255, 168, 168); + transform: scale(1.1, 1.1); } } diff --git a/src/core/database/repository.ts b/src/core/database/repository.ts index bb43497..027f2eb 100644 --- a/src/core/database/repository.ts +++ b/src/core/database/repository.ts @@ -1,15 +1,35 @@ import Database from "bun:sqlite"; import { logger } from "~/core/utils/logger"; -import type { DockerHost } from "~/typings/docker"; -import type { HostStats } from "~/typings/docker"; +import { relayController } from "~/core/docker/relay-controller"; +import type { DockerHost, HostStats } from "~/typings/docker"; +import type { stacks_config } from "~/typings/database"; const db = new Database("dockstatapi.db"); +db.exec("PRAGMA journal_mode = WAL;"); export const dbFunctions = { init() { const startTime = Date.now(); - logger.debug("__task__ __db__ Initializing Database ⏳") db.exec(` + CREATE TABLE IF NOT EXISTS backend_log_entries ( + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + level TEXT, + message TEXT, + file TEXT, + line NUMBER + ); + + CREATE TABLE IF NOT EXISTS stacks_config ( + name TEXT PRIMARY KEY, + version INTEGER, + custom BOOLEAN, + source TEXT, + container_count INTEGER, + stack_prefix TEXT, + automatic_reboot_on_error BOOLEAN, + image_updates BOOLEAN + ); + CREATE TABLE IF NOT EXISTS docker_hosts ( name TEXT, url TEXT, @@ -49,16 +69,11 @@ export const dbFunctions = { keep_data_for NUMBER, fetching_interval NUMBER ); - - CREATE TABLE IF NOT EXISTS backend_log_entries ( - timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, - level TEXT, - message TEXT, - file TEXT, - line NUMBER - ); `); + logger.info("Starting server..."); + + /* * Default values: * - Websocket polling interval 5 seconds @@ -90,6 +105,7 @@ export const dbFunctions = { ); stmt.run("Localhost", "localhost:2375", false); } + logger.debug("__task__ __db__ Initializing Database ⏳") const duration = Date.now() - startTime; logger.debug(`__task__ __db__ Initializing Database ✔️ (${duration}ms)`); }, @@ -301,6 +317,29 @@ export const dbFunctions = { return data }, + deleteOldData(days: number) { + const startTime = Date.now(); + logger.debug("__task__ __db__ Deleting old data ⏳") + if (typeof days !== "number") { + logger.crit("Invalid parameter type for deleteOldData"); + throw new TypeError("Days parameter must be a number"); + } + + const deleteContainerStmt = db.prepare(` + DELETE FROM container_stats + WHERE timestamp < datetime('now', '-' || ? || ' days') + `); + deleteContainerStmt.run(days); + + const deleteLogsStmt = db.prepare(` + DELETE FROM backend_log_entries + WHERE timestamp < datetime('now', '-' || ? || ' days') + `); + deleteLogsStmt.run(days); + const duration = Date.now() - startTime; + logger.debug(`__task__ __db__ Deleting old data ✔️ (${duration}ms)`); + }, + // Stats: addContainerStats( id: string, @@ -347,29 +386,6 @@ export const dbFunctions = { return data }, - deleteOldData(days: number) { - const startTime = Date.now(); - logger.debug("__task__ __db__ Deleting old data ⏳") - if (typeof days !== "number") { - logger.crit("Invalid parameter type for deleteOldData"); - throw new TypeError("Days parameter must be a number"); - } - - const deleteContainerStmt = db.prepare(` - DELETE FROM container_stats - WHERE timestamp < datetime('now', '-' || ? || ' days') - `); - deleteContainerStmt.run(days); - - const deleteLogsStmt = db.prepare(` - DELETE FROM backend_log_entries - WHERE timestamp < datetime('now', '-' || ? || ' days') - `); - deleteLogsStmt.run(days); - const duration = Date.now() - startTime; - logger.debug(`__task__ __db__ Deleting old data ✔️ (${duration}ms)`); - }, - updateHostStats(stats: HostStats) { const startTime = Date.now(); logger.debug("__task__ __db__ Update Host Stats ⏳") @@ -424,4 +440,105 @@ export const dbFunctions = { logger.debug(`__task__ __db__ Update Host stats ✔️ (${duration}ms)`); return data }, -}; + + // Stacks: + // This is the stack config which will be saved in the database, the "real" docker-compose can be found in the designated folder + addStack(stack_config: stacks_config) { + const startTime = Date.now(); + logger.debug("__task__ __db__ Add Stack config ⏳") + + const stmt = db.prepare(` + INSERT INTO stacks_config ( + name, + version, + custom, + source, + container_count, + stack_prefix, + automatic_reboot_on_error, + image_updates + ) + VALUES(?, ?, ?, ?, ?, ?, ?, ?); + `); + const data = stmt.run( + (stack_config.name as any), // I dont fucking know bruh + stack_config.version, + stack_config.custom, + stack_config.source, + stack_config.container_count, + stack_config.stack_prefix, + stack_config.automatic_reboot_on_error, + stack_config.image_updates + ); + + const duration = Date.now() - startTime; + logger.debug(`__task__ __db__ Add Stack config ✔️ (${duration}ms)`); + relayController.stackAdded(); + return data; + }, + + getStacks() { + const startTime = Date.now(); + logger.debug("__task__ __db__ Get Stack config ⏳") + + const stmt = db.prepare(` + SELECT name, version, custom, source, container_count, stack_prefix, automatic_reboot_on_error, image_updates + FROM stacks_config + ORDER BY name DESC + `); + const data = stmt.all(); + + const duration = Date.now() - startTime; + logger.debug(`__task__ __db__ Get Stack config ✔️ (${duration}ms)`); + return data; + }, + + deleteStack(name: string) { + const startTime = Date.now(); + logger.debug("__task__ __db__ Delete Stack config ⏳"); + + const stmt = db.prepare(` + DELETE FROM stacks_config + WHERE name = ?; + `); + const data = stmt.run(name); + + const duration = Date.now() - startTime; + logger.debug(`__task__ __db__ Delete Stack config ✔️ (${duration}ms)`); + relayController.stackDeleted(); + return data; + }, + + updateStack(stack_config: stacks_config) { + const startTime = Date.now(); + logger.debug("__task__ __db__ Update Stack config ⏳"); + + const stmt = db.prepare(` + UPDATE stacks_config + SET + version = ?, + custom = ?, + source = ?, + container_count = ?, + stack_prefix = ?, + automatic_reboot_on_error = ?, + image_updates = ? + WHERE name = ?; + `); + const data = stmt.run( + stack_config.version, + stack_config.custom, + stack_config.source, + stack_config.container_count, + stack_config.stack_prefix, + stack_config.automatic_reboot_on_error, + stack_config.image_updates, + (stack_config.name as any) // Bruh what is this :sob: + ); + + const duration = Date.now() - startTime; + logger.debug(`__task__ __db__ Update Stack config ✔️ (${duration}ms)`); + relayController.stackUpdated(); + return data; + } +}; \ No newline at end of file diff --git a/src/core/docker/client.ts b/src/core/docker/client.ts index da17403..3d5baff 100644 --- a/src/core/docker/client.ts +++ b/src/core/docker/client.ts @@ -18,3 +18,20 @@ export const getDockerClient = (host: DockerHost): Docker => { throw new Error("Invalid Docker host configuration"); } }; + +export const stackClient = (): Docker => { + try { + const docker = new Docker({ + socketPath: "/var/run/docker.sock" + }) + + docker.ping().catch(() => { + throw new Error("Could not ping local Docker-Socket") + }); + + return docker; + } catch (error) { + logger.error(`Could not create Docker client for "/var/run/docker.sock" - ${error as string}`) + throw new Error() + } +} \ No newline at end of file diff --git a/src/core/docker/relay-controller.ts b/src/core/docker/relay-controller.ts new file mode 100644 index 0000000..db8b6bb --- /dev/null +++ b/src/core/docker/relay-controller.ts @@ -0,0 +1,13 @@ +// Import any function here, when any of the specifies functions is detected, it will run said function + +export const relayController = { + stackAdded() { + + }, + stackDeleted() { + + }, + stackUpdated() { + + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 3a4521f..a2dfb81 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,13 +7,12 @@ import { dockerRoutes } from "~/routes/docker-manager"; import { dockerStatsRoutes } from "~/routes/docker-stats"; import { backendLogs } from "~/routes/logs"; import { dockerWebsocketRoutes } from "~/routes/docker-websocket"; +import { stackRoutes } from "./routes/stacks"; import { apiConfigRoutes } from "~/routes/api-config"; import { setSchedules } from "~/core/docker/scheduler"; import staticPlugin from "@elysiajs/static"; console.log("") -logger.info("Starting server..."); - dbFunctions.init(); const DockStatAPI = new Elysia() @@ -36,6 +35,10 @@ const DockStatAPI = new Elysia() name: "Management", description: "Various endpoints for managing DockStatAPI", }, + { + name: "Stacks", + description: "DockStat's Stack functionality", + }, { name: "Utils", description: "Various utilities which might be useful", @@ -49,6 +52,7 @@ const DockStatAPI = new Elysia() .use(backendLogs) .use(dockerWebsocketRoutes) .use(apiConfigRoutes) + .use(stackRoutes) .get("/health", () => ({ status: "healthy" }), { tags: ["Utils"] }) .onError(({ code, set }) => { if (code === 'NOT_FOUND') { @@ -63,7 +67,7 @@ async function startServer() { try { await loadPlugins("./src/plugins"); DockStatAPI.listen(3000, ({ hostname, port }) => { - console.log("") + console.log("----- [ ############## ]") logger.info(`DockStatAPI is running at http://${hostname}:${port}`); logger.info( `Swagger API Documentation available at http://${hostname}:${port}/swagger`, @@ -79,4 +83,5 @@ await setSchedules(); await startServer(); logger.info("Started server"); -console.log("") +console.log("----- [ ############## ]") + diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index b57fbf7..14006eb 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -14,7 +14,6 @@ import { devDependencies, license, } from "~/core/utils/package-json"; -import { describe } from "test"; export const apiConfigRoutes = new Elysia({ prefix: "/config" }) .get( @@ -90,4 +89,8 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) "Error while reading package.json", ); } - }); + }, + { + tags: ["Management"], + }, + ); diff --git a/src/routes/stacks.ts b/src/routes/stacks.ts new file mode 100644 index 0000000..82cdc9d --- /dev/null +++ b/src/routes/stacks.ts @@ -0,0 +1,245 @@ +import { Elysia, error, t } from "elysia"; +import { responseHandler } from "~/core/utils/respone-handler"; +import { + deployStack, + stopStack, + pullStackImages, + restartStack, + getStackStatus, + startStack +} from "~/core/stacks/controller"; +import { dbFunctions } from "~/core/database/repository"; +import { logger } from "~/core/utils/logger"; + +export const stackRoutes = new Elysia({ prefix: "/stacks" }) + .post( + "/deploy", + async ({ set, body }) => { + try { + const isCustom = body.isCustom + ? body.isCustom + : false; + + const image_updates = body.image_updates + ? body.image_updates + : false; + + let errMsg: string = ""; + if (!body.compose_spec) { + errMsg = "compose_spec" + } + + if (!body.automatic_reboot_on_error) { + errMsg = `${errMsg} automatic_reboot_on_error` + } + + if (!body.source) { + errMsg = `${errMsg} source` + } + + if (!body.name) { + errMsg = `${errMsg} name` + } + + if (errMsg) { + errMsg = errMsg.trim(); + errMsg = `Missing values of: ${errMsg.replaceAll(" ", "; ")}` + return responseHandler.error(set, errMsg, errMsg) + } + + await deployStack( + body.compose_spec, + body.name, + body.version, + body.source, + body.automatic_reboot_on_error, + isCustom, + image_updates, + body.stack_prefix + ); + logger.info(`Deployed Stack (${body.name})`) + return responseHandler.ok( + set, + `Stack ${body.name} deployed successfully` + ); + } catch (error: any) { + return responseHandler.error( + set, + error.message || error, + "Error deploying stack" + ); + } + }, + { + detail: { tags: ["Stacks"] }, + body: t.Object({ + compose_spec: t.Any(), + name: t.String(), + version: t.Number(), + automatic_reboot_on_error: t.Boolean(), + isCustom: t.Boolean(), + image_updates: t.Boolean(), + source: t.String(), + stack_prefix: t.Optional(t.String()), + }), + } + ) + .post( + "/start", + async ({ set, body }) => { + try { + if (!body.stack) { + throw new Error("Stack needed") + } + await startStack(body.stack); + logger.info(`Started Stack (${body.stack})`) + return responseHandler.ok( + set, + `Stack ${body.stack} started successfully` + ); + } catch (error: any) { + return responseHandler.error( + set, + error.message || error, + "Error starting stack" + ); + } + }, + { + detail: { tags: ["Stacks"] }, + body: t.Object({ + stack: t.Any(), + }), + } + ) + .post( + "/stop", + async ({ set, body }) => { + try { + if (!body.stack) { + throw new Error("Stack needed") + } + await stopStack(body.stack); + logger.info(`Stopped Stack (${body.stack})`) + return responseHandler.ok( + set, + `Stack ${body.stack} stopped successfully` + ); + } catch (error: any) { + return responseHandler.error( + set, + error.message || error, + "Error stopping stack" + ); + } + }, + { + detail: { tags: ["Stacks"] }, + body: t.Object({ + stack: t.Any(), + }), + } + ) + .post( + "/restart", + async ({ set, body }) => { + try { + if (!body.stack) { + throw new Error("Stack needed") + } + await restartStack(body.stack); + logger.info(`Restarted Stack (${body.stack})`) + return responseHandler.ok( + set, + `Stack ${body.stack} restarted successfully` + ); + } catch (error: any) { + return responseHandler.error( + set, + error.message || error, + "Error restarting stack" + ); + } + }, + { + detail: { tags: ["Stacks"] }, + body: t.Object({ + stack: t.Any(), + }), + } + ) + .post( + "/pull-images", + async ({ set, body }) => { + try { + if (!body.stack) { + throw new Error("Stack needed") + } + await pullStackImages(body.stack); + logger.info(`Pulled Stack images (${body.stack})`) + return responseHandler.ok( + set, + `Images for stack ${body.stack} pulled successfully` + ); + } catch (error: any) { + return responseHandler.error( + set, + error.message || error, + "Error pulling images" + ); + } + }, + { + detail: { tags: ["Stacks"] }, + body: t.Object({ + stack: t.Any(), + }), + } + ) + .get( + "/status", + async ({ set, query }) => { + try { + if (!query.stack_name) { + throw new Error("Stack needed") + } + logger.debug(query.stack_name) + const status = await getStackStatus(query.stack_name); + const res = responseHandler.ok( + set, + `Stack ${query.stack_name} status retrieved successfully` + ); + logger.info("Fetched Stack status") + return { ...res, status: status }; + } catch (error: any) { + return responseHandler.error( + set, + error.message || error, + "Error getting stack status" + ); + } + }, + { + detail: { tags: ["Stacks"] }, + query: t.Object({ + stack_name: t.Any(), + }), + } + ) + .get("/", async ({ set }) => { + try { + const stacks = dbFunctions.getStacks(); + logger.info("Fetched Stacks") + return stacks; + } catch (error: any) { + return responseHandler.error( + set, + error.message || error, + "Error getting stacks" + ); + } + }, + { + detail: { tags: ["Stacks"] }, + } + ); diff --git a/src/typings/database.ts b/src/typings/database.ts index 425fa46..7f9afe6 100644 --- a/src/typings/database.ts +++ b/src/typings/database.ts @@ -11,5 +11,15 @@ interface config { keep_data_for: number; fetching_interval: number; } +interface stacks_config { + name: string; + version: number; + custom: Boolean; + source: string; + container_count: number; + stack_prefix: string; + automatic_reboot_on_error: Boolean; + image_updates: Boolean; +} -export type { backend_log_entries, config }; +export type { backend_log_entries, config, stacks_config }; diff --git a/src/typings/docker-compose.ts b/src/typings/docker-compose.ts new file mode 100644 index 0000000..a554c21 --- /dev/null +++ b/src/typings/docker-compose.ts @@ -0,0 +1,393 @@ +export interface Stack { + compose_spec: ComposeSpec; + name: string + version: number; + source: string; +} + +export interface ComposeSpec { + version?: string; + name?: string; + include?: Include[]; + services?: { [key: string]: Service }; + networks?: { [key: string]: Network }; + volumes?: { [key: string]: Volume }; + secrets?: { [key: string]: Secret }; + configs?: { [key: string]: Config }; + [key: `x-${string}`]: any; +} + +type Include = string | { path: string | string[]; env_file?: string | string[]; project_directory?: string }; + +interface Service { + develop?: Development | null; + deploy?: Deployment | null; + annotations?: ListOrDict; + attach?: boolean | string; + build?: string | { + context?: string; + dockerfile?: string; + dockerfile_inline?: string; + entitlements?: string[]; + args?: ListOrDict; + ssh?: ListOrDict; + labels?: ListOrDict; + cache_from?: string[]; + cache_to?: string[]; + no_cache?: boolean | string; + additional_contexts?: ListOrDict; + network?: string; + pull?: boolean | string; + target?: string; + shm_size?: number | string; + extra_hosts?: ExtraHosts; + isolation?: string; + privileged?: boolean | string; + secrets?: ServiceConfigOrSecret[]; + tags?: string[]; + ulimits?: Ulimits; + platforms?: string[]; + [key: `x-${string}`]: any; + }; + blkio_config?: { + device_read_bps?: BlkioLimit[]; + device_read_iops?: BlkioLimit[]; + device_write_bps?: BlkioLimit[]; + device_write_iops?: BlkioLimit[]; + weight?: number | string; + weight_device?: BlkioWeight[]; + }; + cap_add?: string[]; + cap_drop?: string[]; + cgroup?: 'host' | 'private'; + cgroup_parent?: string; + command?: Command; + configs?: ServiceConfigOrSecret[]; + container_name?: string; + cpu_count?: string | number; + cpu_percent?: string | number; + cpu_shares?: number | string; + cpu_quota?: number | string; + cpu_period?: number | string; + cpu_rt_period?: number | string; + cpu_rt_runtime?: number | string; + cpus?: number | string; + cpuset?: string; + credential_spec?: { + config?: string; + file?: string; + registry?: string; + [key: `x-${string}`]: any; + }; + depends_on?: string[] | { + [service: string]: { + condition: 'service_started' | 'service_healthy' | 'service_completed_successfully'; + restart?: boolean | string; + required?: boolean; + [key: `x-${string}`]: any; + } + }; + device_cgroup_rules?: string[]; + devices?: (string | { + source: string; + target?: string; + permissions?: string; + [key: `x-${string}`]: any; + })[]; + dns?: StringOrList; + dns_opt?: string[]; + dns_search?: StringOrList; + domainname?: string; + entrypoint?: Command; + env_file?: EnvFile; + label_file?: string | string[]; + environment?: ListOrDict; + expose?: (string | number)[]; + extends?: string | { service: string; file?: string }; + external_links?: string[]; + extra_hosts?: ExtraHosts; + gpus?: 'all' | Array<{ + capabilities?: string[]; + count?: string | number; + device_ids?: string[]; + driver?: string; + options?: ListOrDict; + [key: `x-${string}`]: any; + }>; + group_add?: (string | number)[]; + healthcheck?: Healthcheck; + hostname?: string; + image?: string; + init?: boolean | string; + ipc?: string; + isolation?: string; + labels?: ListOrDict; + links?: string[]; + logging?: { + driver?: string; + options?: { [key: string]: string | number | null }; + [key: `x-${string}`]: any; + }; + mac_address?: string; + mem_limit?: number | string; + mem_reservation?: string | number; + mem_swappiness?: number | string; + memswap_limit?: number | string; + network_mode?: string; + networks?: string[] | { + [network: string]: { + aliases?: string[]; + ipv4_address?: string; + ipv6_address?: string; + link_local_ips?: string[]; + mac_address?: string; + driver_opts?: { [key: string]: string | number }; + priority?: number; + [key: `x-${string}`]: any; + } | null; + }; + oom_kill_disable?: boolean | string; + oom_score_adj?: string | number; + pid?: string | null; + pids_limit?: number | string; + platform?: string; + ports?: (number | string | { + name?: string; + mode?: string; + host_ip?: string; + target?: number | string; + published?: string | number; + protocol?: string; + app_protocol?: string; + [key: `x-${string}`]: any; + })[]; + post_start?: ServiceHook[]; + pre_stop?: ServiceHook[]; + privileged?: boolean | string; + profiles?: string[]; + pull_policy?: 'always' | 'never' | 'if_not_present' | 'build' | 'missing'; + read_only?: boolean | string; + restart?: string; + runtime?: string; + scale?: number | string; + security_opt?: string[]; + shm_size?: number | string; + secrets?: ServiceConfigOrSecret[]; + sysctls?: ListOrDict; + stdin_open?: boolean | string; + stop_grace_period?: string; + stop_signal?: string; + storage_opt?: object; + tmpfs?: StringOrList; + tty?: boolean | string; + ulimits?: Ulimits; + user?: string; + uts?: string; + userns_mode?: string; + volumes?: (string | { + type: string; + source?: string; + target?: string; + read_only?: boolean | string; + consistency?: string; + bind?: { + propagation?: string; + create_host_path?: boolean | string; + recursive?: 'enabled' | 'disabled' | 'writable' | 'readonly'; + selinux?: 'z' | 'Z'; + [key: `x-${string}`]: any; + }; + volume?: { + nocopy?: boolean | string; + subpath?: string; + [key: `x-${string}`]: any; + }; + tmpfs?: { + size?: number | string; + mode?: number | string; + [key: `x-${string}`]: any; + }; + [key: `x-${string}`]: any; + })[]; + volumes_from?: string[]; + working_dir?: string; + [key: `x-${string}`]: any; +} + +interface Healthcheck { + disable?: boolean | string; + interval?: string; + retries?: number | string; + test?: string | string[]; + timeout?: string; + start_period?: string; + start_interval?: string; + [key: `x-${string}`]: any; +} + +interface Development { + watch?: Array<{ + path: string; + action: 'rebuild' | 'sync' | 'restart' | 'sync+restart' | 'sync+exec'; + ignore?: string[]; + target?: string; + exec?: ServiceHook; + [key: `x-${string}`]: any; + }>; + [key: `x-${string}`]: any; +} + +interface Deployment { + mode?: string; + endpoint_mode?: string; + replicas?: number | string; + labels?: ListOrDict; + rollback_config?: { + parallelism?: number | string; + delay?: string; + failure_action?: string; + monitor?: string; + max_failure_ratio?: number | string; + order?: 'start-first' | 'stop-first'; + [key: `x-${string}`]: any; + }; + update_config?: { + parallelism?: number | string; + delay?: string; + failure_action?: string; + monitor?: string; + max_failure_ratio?: number | string; + order?: 'start-first' | 'stop-first'; + [key: `x-${string}`]: any; + }; + resources?: { + limits?: { + cpus?: number | string; + memory?: string; + pids?: number | string; + [key: `x-${string}`]: any; + }; + reservations?: { + cpus?: number | string; + memory?: string; + generic_resources?: Array<{ + discrete_resource_spec?: { + kind?: string; + value?: number | string; + [key: `x-${string}`]: any; + }; + [key: `x-${string}`]: any; + }>; + devices?: Array<{ + capabilities?: string[]; + count?: string | number; + device_ids?: string[]; + driver?: string; + options?: ListOrDict; + [key: `x-${string}`]: any; + }>; + [key: `x-${string}`]: any; + }; + [key: `x-${string}`]: any; + }; + restart_policy?: { + condition?: string; + delay?: string; + max_attempts?: number | string; + window?: string; + [key: `x-${string}`]: any; + }; + placement?: { + constraints?: string[]; + preferences?: Array<{ + spread?: string; + [key: `x-${string}`]: any; + }>; + max_replicas_per_node?: number | string; + [key: `x-${string}`]: any; + }; + [key: `x-${string}`]: any; +} + +type Command = string | string[] | null; +type EnvFile = string | Array; +type StringOrList = string | string[]; +type ListOrDict = { [key: string]: string | number | boolean | null } | string[]; +type ExtraHosts = { [host: string]: string | string[] } | string[]; +interface BlkioLimit { path: string; rate: number | string; } +interface BlkioWeight { path: string; weight: number | string; } +type ServiceConfigOrSecret = string | { + source: string; + target?: string; + uid?: string; + gid?: string; + mode?: number | string; + [key: `x-${string}`]: any; +}; +type Ulimits = { [key: string]: number | string | { hard: number | string; soft: number | string } }; + +interface ServiceHook { + command?: Command; + user?: string; + privileged?: boolean | string; + working_dir?: string; + environment?: ListOrDict; + [key: `x-${string}`]: any; +} + +interface Network { + name?: string; + driver?: string; + driver_opts?: { [key: string]: string | number }; + ipam?: { + driver?: string; + config?: Array<{ + subnet?: string; + ip_range?: string; + gateway?: string; + aux_addresses?: { [key: string]: string }; + [key: `x-${string}`]: any; + }>; + options?: { [key: string]: string }; + [key: `x-${string}`]: any; + }; + external?: boolean | string | { name?: string;[key: `x-${string}`]: any }; + internal?: boolean | string; + enable_ipv4?: boolean | string; + enable_ipv6?: boolean | string; + attachable?: boolean | string; + labels?: ListOrDict; + [key: `x-${string}`]: any; +} + +interface Volume { + name?: string; + driver?: string; + driver_opts?: { [key: string]: string | number }; + external?: boolean | string | { name?: string;[key: `x-${string}`]: any }; + labels?: ListOrDict; + [key: `x-${string}`]: any; +} + +interface Secret { + name?: string; + environment?: string; + file?: string; + external?: boolean | string | { name?: string;[key: string]: any }; + labels?: ListOrDict; + driver?: string; + driver_opts?: { [key: string]: string | number }; + template_driver?: string; + [key: `x-${string}`]: any; +} + +interface Config { + name?: string; + content?: string; + environment?: string; + file?: string; + external?: boolean | string | { name?: string;[key: string]: any }; + labels?: ListOrDict; + template_driver?: string; + [key: `x-${string}`]: any; +} \ No newline at end of file From d2cc5016a17d84f5bc766638af3b88330f06ec36 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 11 Mar 2025 22:16:06 +0100 Subject: [PATCH 149/324] Fix: ignore adjustments --- .dockerignore | 4 +- .gitignore | 4 +- src/core/stacks/controller.ts | 135 ++++++++++++++++++++++++++++++++++ 3 files changed, 139 insertions(+), 4 deletions(-) create mode 100644 src/core/stacks/controller.ts diff --git a/.dockerignore b/.dockerignore index 152a619..1e14091 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,4 @@ *.db* -stacks -node_modules +/stacks +/node_modules *.md \ No newline at end of file diff --git a/.gitignore b/.gitignore index 964a918..aa426ba 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ *.db* -stacks -node_modules \ No newline at end of file +/stacks +/node_modules \ No newline at end of file diff --git a/src/core/stacks/controller.ts b/src/core/stacks/controller.ts new file mode 100644 index 0000000..133b867 --- /dev/null +++ b/src/core/stacks/controller.ts @@ -0,0 +1,135 @@ +import { dbFunctions } from "../database/repository"; +import YAML from "yaml"; +import { logger } from "../utils/logger"; +import DockerCompose from "docker-compose"; +import type { Stack, ComposeSpec } from "~/typings/docker-compose"; +import type { stacks_config } from "~/typings/database"; + +async function getStackPath(stack: Stack): Promise { + const stackName = stack.name.trim().replace(/\s+/g, "_"); + return `stacks/${stackName}`; +} + +async function createStackYAML(compose_spec: Stack): Promise { + const yaml = YAML.stringify(compose_spec.compose_spec); + const stackPath = await getStackPath(compose_spec); + await Bun.write(`${stackPath}/docker-compose.yaml`, yaml, { createPath: true }); +} + +export async function deployStack( + stack: ComposeSpec, + name: string, + version: number, + source: string, + automatic_reboot_on_error: boolean, + isCustom: boolean, + image_updates: boolean, + stack_prefix?: string +): Promise { + try { + logger.debug(`Deploying Stack: ${JSON.stringify(stack)}`) + + const serviceCount = stack.services + ? Object.keys(stack.services).length + : 0; + + const resolvedPrefix = stack_prefix ?? ""; + + const stack_config: stacks_config = { + name: name, + version: version, + source, + stack_prefix: resolvedPrefix, + automatic_reboot_on_error, + container_count: serviceCount, + custom: isCustom, + image_updates, + }; + + if (!stack.name) { + logger.debug(`${JSON.stringify(stack)}`) + throw new Error("Stack name needed") + } + + dbFunctions.addStack(stack_config); + + const stackYaml: Stack = { + name: name, + source: source, + version: version, + compose_spec: stack, + } + await createStackYAML(stackYaml); + const stackPath = await getStackPath(stackYaml); + await DockerCompose.upAll({ cwd: stackPath }); + } catch (error: any) { + throw new Error(`Error while deploying Stack: ${error.message || error}`); + } +} + +export async function stopStack(stack_name: string): Promise { + try { + const stack = { + name: stack_name + } + const stackPath = await getStackPath(stack as Stack); + await DockerCompose.downAll({ cwd: stackPath }); + } catch (error: any) { + throw new Error(`Error while stopping stack "${stack_name}": ${error.message || error}`); + } +} + +export async function startStack(stack_name: string): Promise { + try { + const stack = { + name: stack_name + } + const stackPath = await getStackPath(stack as Stack); + await DockerCompose.upAll({ cwd: stackPath }); + } catch (error: any) { + throw new Error(`Error while starting stack "${stack_name}": ${error.message || error}`); + } +} + +export async function pullStackImages(stack_name: string): Promise { + try { + const stack = { + name: stack_name + } + const stackPath = await getStackPath(stack as Stack); + await DockerCompose.pullAll({ cwd: stackPath }); + } catch (error: any) { + throw new Error(`Error while pulling images for stack "${stack_name}": ${error.message || error}`); + } +} + +export async function restartStack(stack_name: string): Promise { + try { + const stack = { + name: stack_name + } + const stackPath = await getStackPath(stack as Stack); + await DockerCompose.restartAll({ cwd: stackPath }); + } catch (error: any) { + throw new Error(`Error while restarting stack "${stack_name}": ${error.message || error}`); + } +} + +export async function getStackStatus(stack_name: string): Promise { + try { + logger.debug("Retrieving status for Stack:", stack_name); + const stackYaml = { name: stack_name }; + const stackPath = await getStackPath(stackYaml as Stack); + const rawStatus = await DockerCompose.ps({ cwd: stackPath }); + + const transformedStatus = rawStatus.data.services.reduce((acc: any, service: any) => { + acc[(service.name)] = service.state; + return acc; + }, {}); + + return transformedStatus; + } catch (error: any) { + throw new Error(`Error while retrieving status for stack "${stack_name}": ${error.message || error}`); + } +} + From 1b6b04adfd22f5915ee4cdceae3006bac31d31b1 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 11 Mar 2025 22:20:13 +0100 Subject: [PATCH 150/324] Update README.md Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9784f98..ad1371b 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ The DockStat API provides the following endpoints: - `DELETE /logs`: Clear all backend logs. - `DELETE /logs/:level`: Clear logs by log level. -### Webocket +### Websocket - `WS(S) /docker/stats`: Retrieve the current API configuration. ## API From a07499d70cc49c43ac599773a2cca85ab0126388 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 11 Mar 2025 22:21:57 +0100 Subject: [PATCH 151/324] Inline variable that is immediately returned (inline-immediately-returned-variable) Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- src/core/stacks/controller.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/core/stacks/controller.ts b/src/core/stacks/controller.ts index 133b867..2ff5edb 100644 --- a/src/core/stacks/controller.ts +++ b/src/core/stacks/controller.ts @@ -122,12 +122,11 @@ export async function getStackStatus(stack_name: string): Promise { const stackPath = await getStackPath(stackYaml as Stack); const rawStatus = await DockerCompose.ps({ cwd: stackPath }); - const transformedStatus = rawStatus.data.services.reduce((acc: any, service: any) => { - acc[(service.name)] = service.state; - return acc; - }, {}); + return rawStatus.data.services.reduce((acc: any, service: any) => { + acc[(service.name)] = service.state; + return acc; + }, {}); - return transformedStatus; } catch (error: any) { throw new Error(`Error while retrieving status for stack "${stack_name}": ${error.message || error}`); } From 26ebd1e8d330d381696930e62e3754ecb74b8f2f Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 11 Mar 2025 22:22:34 +0100 Subject: [PATCH 152/324] Avoid unneeded ternary statements (simplify-ternary) Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- src/routes/stacks.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/routes/stacks.ts b/src/routes/stacks.ts index 82cdc9d..3108e0c 100644 --- a/src/routes/stacks.ts +++ b/src/routes/stacks.ts @@ -20,9 +20,8 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) ? body.isCustom : false; - const image_updates = body.image_updates - ? body.image_updates - : false; + const image_updates = body.image_updates || false; + let errMsg: string = ""; if (!body.compose_spec) { From 35f70f831e3733e09662143292bd118d9f217f69 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 11 Mar 2025 22:22:44 +0100 Subject: [PATCH 153/324] Avoid unneeded ternary statements (simplify-ternary) Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- src/routes/stacks.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/routes/stacks.ts b/src/routes/stacks.ts index 3108e0c..f113d14 100644 --- a/src/routes/stacks.ts +++ b/src/routes/stacks.ts @@ -16,9 +16,8 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) "/deploy", async ({ set, body }) => { try { - const isCustom = body.isCustom - ? body.isCustom - : false; + const isCustom = body.isCustom || false; + const image_updates = body.image_updates || false; From a827c0ab844f15f1188f787ee003412e86d08a10 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 11 Mar 2025 22:33:07 +0100 Subject: [PATCH 154/324] Inline variable that is immediately returned (inline-immediately-returned-variable) Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- src/routes/api-config.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index 14006eb..5be4dfb 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -70,18 +70,18 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) .get("/package", async ({ set }) => { try { logger.debug("Fetching package.json"); - const data = { - version: version, - description: description, - license: license, - authorName: authorName, - authorEmail: authorEmail, - authorWebsite: authorWebsite, - contributers: contributers, - dependencies: dependencies, - devDependencies: devDependencies, - }; - return data; + return { + version: version, + description: description, + license: license, + authorName: authorName, + authorEmail: authorEmail, + authorWebsite: authorWebsite, + contributers: contributers, + dependencies: dependencies, + devDependencies: devDependencies, + }; + } catch (error) { return responseHandler.error( set, From 4155566dac5af47a5dedf63af02fac45bb13c647 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 11 Mar 2025 22:40:03 +0100 Subject: [PATCH 155/324] Fix: Code Quality --- src/core/utils/package-json.ts | 9 ++------- src/routes/docker-websocket.ts | 21 ++++++++++++++++----- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/core/utils/package-json.ts b/src/core/utils/package-json.ts index 5b84757..3872d56 100644 --- a/src/core/utils/package-json.ts +++ b/src/core/utils/package-json.ts @@ -1,14 +1,9 @@ import packageJson from "~/../package.json"; +const { version, description, license, contributors, dependencies, devDependencies } = packageJson; -const version = packageJson.version; -const description = packageJson.description; const authorName = packageJson.author.name; const authorEmail = packageJson.author.email; const authorWebsite = packageJson.author.url; -const license = packageJson.license; -const contributers = packageJson.contributors; -const dependencies = packageJson.dependencies; -const devDependencies = packageJson.devDependencies; export { version, @@ -17,7 +12,7 @@ export { authorEmail, authorWebsite, license, - contributers, + contributors, dependencies, devDependencies, }; diff --git a/src/routes/docker-websocket.ts b/src/routes/docker-websocket.ts index f1e6e68..e040645 100644 --- a/src/routes/docker-websocket.ts +++ b/src/routes/docker-websocket.ts @@ -62,7 +62,9 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( }, 30000); for (const host of hosts) { - if (!(socket as any).isOpen) break; + if (!(socket as any).isOpen) { + break + }; logger.debug(`Processing host: ${host.name}`); @@ -77,7 +79,9 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( ); for (const containerInfo of containers) { - if (!(socket as any).isOpen) break; + if (!(socket as any).isOpen) { + break + }; logger.debug( `Processing container ${containerInfo.Id} on host ${host.name}`, @@ -109,8 +113,13 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( statsStream .pipe(splitStream) .on("data", (line: string) => { - if (socket.readyState !== 1) return; // 1 = OPEN state - if (!line) return; + // 1 = OPEN state + if (socket.readyState !== 1) { + return + }; + if (!line) { + return + }; try { const stats = JSON.parse(line); const cpuUsage = calculateCpuPercent(stats); @@ -187,7 +196,9 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( }, message(socket, message) { - if (message === "pong") return; + if (message === "pong") { + return + }; }, close(socket, code, reason) { From 1ba080d1aba00701b7cea64c22417e7e425fa166 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 11 Mar 2025 22:44:47 +0100 Subject: [PATCH 156/324] Fix: Adjust Readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ad1371b..ed73de8 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ The DockStat API exposes the following endpoints: ## License This project is licensed under the CC BY-NC 4.0 License. -[cc-by-nc-image]: https://licensebuttons.net/l/by-nc/4.0/88x31.png +[cc-by-nc-image]https://licensebuttons.net/l/by-nc/4.0/88x31.png ## Testing From 3b4696a9b212ee682b129e073716f60794761172 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 11 Mar 2025 22:45:21 +0100 Subject: [PATCH 157/324] Fix: Adjust Rreadme (forgot how markdown works) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ed73de8..eae97c4 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ The DockStat API exposes the following endpoints: ## License This project is licensed under the CC BY-NC 4.0 License. -[cc-by-nc-image]https://licensebuttons.net/l/by-nc/4.0/88x31.png +[cc-by-nc-image](https://licensebuttons.net/l/by-nc/4.0/88x31.png) ## Testing From 03c872d9cdadae274bc2f58a3cc4d4d00ce012e0 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 11 Mar 2025 22:45:48 +0100 Subject: [PATCH 158/324] Fix: Adjust Readme (this is getting sill) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index eae97c4..3ea8187 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ The DockStat API exposes the following endpoints: ## License This project is licensed under the CC BY-NC 4.0 License. -[cc-by-nc-image](https://licensebuttons.net/l/by-nc/4.0/88x31.png) +![cc-by-nc-image](https://licensebuttons.net/l/by-nc/4.0/88x31.png) ## Testing From 745856e6b0e8f3ac5872f8a7ec88f8e85394e53b Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 11 Mar 2025 23:01:32 +0100 Subject: [PATCH 159/324] Fix: Fixes #38 --- src/core/plugins/loader.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/core/plugins/loader.ts b/src/core/plugins/loader.ts index 26d00e5..db77bed 100644 --- a/src/core/plugins/loader.ts +++ b/src/core/plugins/loader.ts @@ -16,7 +16,9 @@ export async function loadPlugins(pluginDir: string) { const files = fs.readdirSync(pluginPath); for (const file of files) { - if (!file.endsWith(".plugin.ts")) continue; + if (!file.endsWith(".plugin.ts")) { + continue + }; const absolutePath = path.join(pluginPath, file); try { From ba507e2d45338bf7c21f860a8d6c29b42219a273 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 11 Mar 2025 23:04:12 +0100 Subject: [PATCH 160/324] Fix: Fixes #37 --- src/routes/api-config.ts | 22 +++++++++++----------- src/routes/stacks.ts | 20 ++++++++------------ 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index 5be4dfb..e618551 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -8,7 +8,7 @@ import { authorEmail, authorName, authorWebsite, - contributers, + contributors, dependencies, description, devDependencies, @@ -71,16 +71,16 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) try { logger.debug("Fetching package.json"); return { - version: version, - description: description, - license: license, - authorName: authorName, - authorEmail: authorEmail, - authorWebsite: authorWebsite, - contributers: contributers, - dependencies: dependencies, - devDependencies: devDependencies, - }; + version: version, + description: description, + license: license, + authorName: authorName, + authorEmail: authorEmail, + authorWebsite: authorWebsite, + contributors: contributors, + dependencies: dependencies, + devDependencies: devDependencies, + }; } catch (error) { return responseHandler.error( diff --git a/src/routes/stacks.ts b/src/routes/stacks.ts index f113d14..600dec5 100644 --- a/src/routes/stacks.ts +++ b/src/routes/stacks.ts @@ -22,27 +22,23 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) const image_updates = body.image_updates || false; - let errMsg: string = ""; + let missingParams: string[] = []; if (!body.compose_spec) { - errMsg = "compose_spec" + missingParams.push("compose_spec"); } - if (!body.automatic_reboot_on_error) { - errMsg = `${errMsg} automatic_reboot_on_error` + missingParams.push("automatic_reboot_on_error"); } - if (!body.source) { - errMsg = `${errMsg} source` + missingParams.push("source"); } - if (!body.name) { - errMsg = `${errMsg} name` + missingParams.push("name"); } - if (errMsg) { - errMsg = errMsg.trim(); - errMsg = `Missing values of: ${errMsg.replaceAll(" ", "; ")}` - return responseHandler.error(set, errMsg, errMsg) + if (missingParams.length > 0) { + const errMsg = `Missing values of: ${missingParams.join("; ")}`; + return responseHandler.error(set, errMsg, errMsg); } await deployStack( From cf33ba6b70dee5dd9d660d1233f6baa3a408020a Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 12 Mar 2025 19:19:06 +0100 Subject: [PATCH 161/324] Fix: Fixes #34 --- src/core/database/repository.ts | 5 +++-- src/typings/database.ts | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/core/database/repository.ts b/src/core/database/repository.ts index 027f2eb..899f7b7 100644 --- a/src/core/database/repository.ts +++ b/src/core/database/repository.ts @@ -460,8 +460,9 @@ export const dbFunctions = { ) VALUES(?, ?, ?, ?, ?, ?, ?, ?); `); + const data = stmt.run( - (stack_config.name as any), // I dont fucking know bruh + stack_config.name, stack_config.version, stack_config.custom, stack_config.source, @@ -533,7 +534,7 @@ export const dbFunctions = { stack_config.stack_prefix, stack_config.automatic_reboot_on_error, stack_config.image_updates, - (stack_config.name as any) // Bruh what is this :sob: + stack_config.name ); const duration = Date.now() - startTime; diff --git a/src/typings/database.ts b/src/typings/database.ts index 7f9afe6..3d95b35 100644 --- a/src/typings/database.ts +++ b/src/typings/database.ts @@ -14,12 +14,12 @@ interface config { interface stacks_config { name: string; version: number; - custom: Boolean; + custom: boolean; source: string; container_count: number; stack_prefix: string; - automatic_reboot_on_error: Boolean; - image_updates: Boolean; + automatic_reboot_on_error: boolean; + image_updates: boolean; } export type { backend_log_entries, config, stacks_config }; From 2e1d948327248b3e2a95bd33fac9efa3a1acbc7e Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 12 Mar 2025 19:57:44 +0100 Subject: [PATCH 162/324] Fix: Fixes #39 --- src/core/database/helper.ts | 17 + src/core/database/repository.ts | 676 ++++++++++++++++---------------- src/core/utils/logger.ts | 27 +- 3 files changed, 379 insertions(+), 341 deletions(-) create mode 100644 src/core/database/helper.ts diff --git a/src/core/database/helper.ts b/src/core/database/helper.ts new file mode 100644 index 0000000..3bea044 --- /dev/null +++ b/src/core/database/helper.ts @@ -0,0 +1,17 @@ +import { logger } from "../utils/logger"; + +export function executeDbOperation( + label: string, + operation: () => T, + validate?: () => void +): T { + const startTime = Date.now(); + logger.debug(`__task__ __db__ ${label} �3`); + if (validate) { + validate(); + } + const result = operation(); + const duration = Date.now() - startTime; + logger.debug(`__task__ __db__ ${label} �4f (${duration}ms)`); + return result; +} \ No newline at end of file diff --git a/src/core/database/repository.ts b/src/core/database/repository.ts index 899f7b7..56543cf 100644 --- a/src/core/database/repository.ts +++ b/src/core/database/repository.ts @@ -1,3 +1,4 @@ +import { executeDbOperation } from "./helper"; import Database from "bun:sqlite"; import { logger } from "~/core/utils/logger"; import { relayController } from "~/core/docker/relay-controller"; @@ -111,39 +112,42 @@ export const dbFunctions = { }, addDockerHost(hostId: string, url: string, secure: boolean) { - const startTime = Date.now(); - logger.debug("__task__ __db__ Adding Docker Host ⏳") - if ( - typeof hostId !== "string" || - typeof url !== "string" || - typeof secure !== "boolean" - ) { - logger.crit("Invalid parameter types for addDockerHost"); - throw new TypeError("Invalid parameter types for addDockerHost"); - } - - const stmt = db.prepare(` + return executeDbOperation( + "Add Docker Host", + () => { + const stmt = db.prepare(` INSERT INTO docker_hosts (name, url, secure) VALUES (?, ?, ?) `); - const data = stmt.run(hostId, url, secure); - const duration = Date.now() - startTime; - logger.debug(`__task__ __db__ Adding Docker Host ✔️ (${duration}ms)`); - return data + return stmt.run(hostId, url, secure); + }, + () => { + if ( + typeof hostId !== "string" || + typeof url !== "string" || + typeof secure !== "boolean" + ) { + logger.error("Invalid parameter types for addDockerHost"); + throw new TypeError("Invalid parameter types for addDockerHost"); + } + } + ); }, getDockerHosts(): DockerHost[] { - const startTime = Date.now(); - logger.debug("__task__ __db__ Getting Docker Host ⏳") - const stmt = db.prepare(` - SELECT name, url, secure - FROM docker_hosts - ORDER BY name DESC - `); - const data = stmt.all(); - const duration = Date.now() - startTime; - logger.debug(`__task__ __db__ Getting Docker Host ✔️ (${duration}ms)`); - return data as DockerHost[]; + return executeDbOperation( + "Get Docker Hosts", + () => { + const stmt = db.prepare(` + SELECT name, url, secure + FROM docker_hosts + ORDER BY name DESC + `); + const data = stmt.all(); + return data as DockerHost[]; + }, + () => { } + ); }, addLogEntry: ( @@ -170,177 +174,192 @@ export const dbFunctions = { }, getAllLogs() { - const startTime = Date.now(); - logger.debug("__task__ __db__ Getting all Logs ⏳") - const stmt = db.prepare(` + return executeDbOperation( + "Get All Logs", + () => { + const stmt = db.prepare(` SELECT timestamp, level, message, file, line FROM backend_log_entries ORDER BY timestamp DESC `); - const data = stmt.all(); - const duration = Date.now() - startTime; - logger.debug(`__task__ __db__ Getting all Logs ✔️ (${duration}ms)`); - return data + const data = stmt.all(); + return data; + }, + () => { } + ); }, getLogsByLevel(level: string) { - const startTime = Date.now(); - logger.debug("__task__ __db__ Getting level-logs ⏳") - if (typeof level !== "string") { - logger.crit("Level parameter must be a string"); - throw new TypeError("Level parameter must be a string"); - } - - const stmt = db.prepare(` + return executeDbOperation( + "Get Logs By Level", + () => { + const stmt = db.prepare(` SELECT timestamp, level, message, file, line FROM backend_log_entries WHERE level = ? ORDER BY timestamp DESC `); - const data = stmt.all(level); - const duration = Date.now() - startTime; - logger.debug(`__task__ __db__ Getting level-logs ✔️ (${duration}ms)`); - return data + const data = stmt.all(level); + return data; + }, + () => { + if (typeof level !== "string") { + logger.error("Level parameter must be a string"); + throw new TypeError("Level parameter must be a string"); + } + } + ); }, updateDockerHost(name: string, url: string, secure: boolean) { - const startTime = Date.now(); - logger.debug("__task__ __db__ Updating Docker Host ⏳") - if ( - typeof name !== "string" || - typeof url !== "string" || - typeof secure !== "boolean" - ) { - logger.crit("Invalid parameter types for updateDockerHost"); - throw new TypeError("Invalid parameter types for updateDockerHost"); - } - - const stmt = db.prepare(` - UPDATE docker_hosts - SET url = ?, secure = ? - WHERE name = ? - `); - const data = stmt.run(url, secure, name); - const duration = Date.now() - startTime; - logger.debug(`__task__ __db__ Updating Docker Host ✔️ (${duration}ms)`); - return data + return executeDbOperation( + "Update Docker Host", + () => { + const stmt = db.prepare(` + UPDATE docker_hosts + SET url = ?, secure = ? + WHERE name = ? + `); + const data = stmt.run(url, secure, name); + return data; + }, + () => { + if ( + typeof name !== "string" || + typeof url !== "string" || + typeof secure !== "boolean" + ) { + logger.error("Invalid parameter types for updateDockerHost"); + throw new TypeError("Invalid parameter types for updateDockerHost"); + } + } + ); }, deleteDockerHost(name: string) { - const startTime = Date.now(); - logger.debug("__task__ __db__ Deleting Docker Host ⏳") - if (typeof name !== "string") { - logger.crit("Invalid parameter type for deleteDockerHost"); - throw new TypeError("Name parameter must be a string"); - } - - const stmt = db.prepare(` - DELETE FROM docker_hosts - WHERE name = ? - `); - const data = stmt.run(name); - const duration = Date.now() - startTime; - logger.debug(`__task__ __db__ Deleting Docker Host ✔️ (${duration}ms)`); - return data + return executeDbOperation( + "Delete Docker Host", + () => { + const stmt = db.prepare(` + DELETE FROM docker_hosts + WHERE name = ? + `); + const data = stmt.run(name); + return data; + }, + () => { + if (typeof name !== "string") { + logger.error("Invalid parameter type for deleteDockerHost"); + throw new TypeError("Name parameter must be a string"); + } + } + ); }, clearAllLogs() { - const startTime = Date.now(); - logger.debug("__task__ __db__ Clearing all Logs ⏳") - const stmt = db.prepare(` - DELETE FROM backend_log_entries - `); - const data = stmt.run(); - const duration = Date.now() - startTime; - logger.debug(`__task__ __db__ Clearing all Logs ✔️ (${duration}ms)`); - return data + return executeDbOperation( + "Clear All Logs", + () => { + const stmt = db.prepare(` + DELETE FROM backend_log_entries + `); + const data = stmt.run(); + return data; + }, + () => { } + ); }, clearLogsByLevel(level: string) { - const startTime = Date.now(); - logger.debug("__task__ __db__ Clearing all logs by level ⏳") - if (typeof level !== "string") { - logger.crit("Invalid parameter type for clearLogsByLevel"); - throw new TypeError("Level parameter must be a string"); - } - - const stmt = db.prepare(` - DELETE FROM backend_log_entries - WHERE level = ? - `); - const data = stmt.run(level); - const duration = Date.now() - startTime; - logger.debug(`__task__ __db__ Clearing all logs by level ✔️ (${duration}ms)`); - return data + return executeDbOperation( + "Clear Logs By Level", + () => { + const stmt = db.prepare(` + DELETE FROM backend_log_entries + WHERE level = ? + `); + const data = stmt.run(level); + return data; + }, + () => { + if (typeof level !== "string") { + logger.error("Invalid parameter type for clearLogsByLevel"); + throw new TypeError("Level parameter must be a string"); + } + } + ); }, updateConfig( polling_rate: number, fetching_interval: number, - keep_data_for: number, + keep_data_for: number ) { - const startTime = Date.now(); - logger.debug("__task__ __db__ Updating config ⏳") - if ( - typeof polling_rate !== "number" || - typeof fetching_interval !== "number" || - typeof keep_data_for !== "number" - ) { - logger.crit("Invalid parameter types for updateConfig"); - throw new TypeError("Invalid parameter types for updateConfig"); - } - - const stmt = db.prepare(` - UPDATE config - SET polling_rate = ?, - fetching_interval = ?, - keep_data_for = ? - `); - - const data = stmt.run(polling_rate, fetching_interval, keep_data_for); - const duration = Date.now() - startTime; - logger.debug(`__task__ __db__ Updating config ✔️ (${duration}ms)`); - return data + return executeDbOperation( + "Update Config", + () => { + const stmt = db.prepare(` + UPDATE config + SET polling_rate = ?, + fetching_interval = ?, + keep_data_for = ? + `); + const data = stmt.run(polling_rate, fetching_interval, keep_data_for); + return data; + }, + () => { + if ( + typeof polling_rate !== "number" || + typeof fetching_interval !== "number" || + typeof keep_data_for !== "number" + ) { + logger.error("Invalid parameter types for updateConfig"); + throw new TypeError("Invalid parameter types for updateConfig"); + } + } + ); }, getConfig() { - const startTime = Date.now(); - logger.debug("__task__ __db__ Getting config ⏳") - const stmt = db.prepare(` - SELECT polling_rate, keep_data_for, fetching_interval - FROM config - `); - - const data = stmt.all(); - const duration = Date.now() - startTime; - logger.debug(`__task__ __db__ Getting config ✔️ (${duration}ms)`); - return data + return executeDbOperation( + "Get Config", + () => { + const stmt = db.prepare(` + SELECT polling_rate, keep_data_for, fetching_interval + FROM config + `); + const data = stmt.all(); + return data; + }, + () => { } + ); }, deleteOldData(days: number) { - const startTime = Date.now(); - logger.debug("__task__ __db__ Deleting old data ⏳") - if (typeof days !== "number") { - logger.crit("Invalid parameter type for deleteOldData"); - throw new TypeError("Days parameter must be a number"); - } - - const deleteContainerStmt = db.prepare(` - DELETE FROM container_stats - WHERE timestamp < datetime('now', '-' || ? || ' days') - `); - deleteContainerStmt.run(days); + return executeDbOperation( + "Delete Old Data", + () => { + const deleteContainerStmt = db.prepare(` + DELETE FROM container_stats + WHERE timestamp < datetime('now', '-' || ? || ' days') + `); + deleteContainerStmt.run(days); - const deleteLogsStmt = db.prepare(` - DELETE FROM backend_log_entries - WHERE timestamp < datetime('now', '-' || ? || ' days') - `); - deleteLogsStmt.run(days); - const duration = Date.now() - startTime; - logger.debug(`__task__ __db__ Deleting old data ✔️ (${duration}ms)`); + const deleteLogsStmt = db.prepare(` + DELETE FROM backend_log_entries + WHERE timestamp < datetime('now', '-' || ? || ' days') + `); + deleteLogsStmt.run(days); + }, + () => { + if (typeof days !== "number") { + logger.error("Invalid parameter type for deleteOldData"); + throw new TypeError("Days parameter must be a number"); + } + } + ); }, - // Stats: addContainerStats( id: string, hostId: string, @@ -349,197 +368,198 @@ export const dbFunctions = { status: string, state: string, cpu_usage: number, - memory_usage: number, + memory_usage: number ) { - const startTime = Date.now(); - logger.debug("__task__ __db__ Adding container statistics ⏳") - if ( - typeof id !== "string" || - typeof hostId !== "string" || - typeof name !== "string" || - typeof image !== "string" || - typeof status !== "string" || - typeof state !== "string" || - typeof cpu_usage !== "number" || - typeof memory_usage !== "number" - ) { - logger.crit("Invalid parameter types for addContainerStats"); - throw new TypeError("Invalid parameter types for addContainerStats"); - } - - const stmt = db.prepare(` - INSERT INTO container_stats (id, hostId, name, image, status, state, cpu_usage, memory_usage) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - `); - const data = stmt.run( - id, - hostId, - name, - image, - status, - state, - cpu_usage, - memory_usage, + return executeDbOperation( + "Add Container Stats", + () => { + const stmt = db.prepare(` + INSERT INTO container_stats (id, hostId, name, image, status, state, cpu_usage, memory_usage) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `); + const data = stmt.run( + id, + hostId, + name, + image, + status, + state, + cpu_usage, + memory_usage + ); + return data; + }, + () => { + if ( + typeof id !== "string" || + typeof hostId !== "string" || + typeof name !== "string" || + typeof image !== "string" || + typeof status !== "string" || + typeof state !== "string" || + typeof cpu_usage !== "number" || + typeof memory_usage !== "number" + ) { + logger.error("Invalid parameter types for addContainerStats"); + throw new TypeError("Invalid parameter types for addContainerStats"); + } + } ); - const duration = Date.now() - startTime; - logger.debug(`__task__ __db__ Adding container statistics ✔️ (${duration}ms)`); - return data }, updateHostStats(stats: HostStats) { - const startTime = Date.now(); - logger.debug("__task__ __db__ Update Host Stats ⏳") - const labelsJson = JSON.stringify(stats.labels); - const stmt = db.prepare(` - INSERT INTO host_stats ( - hostId, - dockerVersion, - apiVersion, - os, - architecture, - totalMemory, - totalCPU, - labels, - containers, - containersRunning, - containersStopped, - containersPaused, - images - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ON CONFLICT(hostId) DO UPDATE SET - dockerVersion = excluded.dockerVersion, - apiVersion = excluded.apiVersion, - os = excluded.os, - architecture = excluded.architecture, - totalMemory = excluded.totalMemory, - totalCPU = excluded.totalCPU, - labels = excluded.labels, - containers = excluded.containers, - containersRunning = excluded.containersRunning, - containersStopped = excluded.containersStopped, - containersPaused = excluded.containersPaused, - images = excluded.images; - `); - const data = stmt.run( - stats.hostId, - stats.dockerVersion, - stats.apiVersion, - stats.os, - stats.architecture, - stats.totalMemory, - stats.totalCPU, - labelsJson, - stats.containers, - stats.containersRunning, - stats.containersStopped, - stats.containersPaused, - stats.images, + return executeDbOperation( + "Update Host Stats", + () => { + const labelsJson = JSON.stringify(stats.labels); + const stmt = db.prepare(` + INSERT INTO host_stats ( + hostId, + dockerVersion, + apiVersion, + os, + architecture, + totalMemory, + totalCPU, + labels, + containers, + containersRunning, + containersStopped, + containersPaused, + images + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(hostId) DO UPDATE SET + dockerVersion = excluded.dockerVersion, + apiVersion = excluded.apiVersion, + os = excluded.os, + architecture = excluded.architecture, + totalMemory = excluded.totalMemory, + totalCPU = excluded.totalCPU, + labels = excluded.labels, + containers = excluded.containers, + containersRunning = excluded.containersRunning, + containersStopped = excluded.containersStopped, + containersPaused = excluded.containersPaused, + images = excluded.images; + `); + const data = stmt.run( + stats.hostId, + stats.dockerVersion, + stats.apiVersion, + stats.os, + stats.architecture, + stats.totalMemory, + stats.totalCPU, + labelsJson, + stats.containers, + stats.containersRunning, + stats.containersStopped, + stats.containersPaused, + stats.images + ); + return data; + }, + () => { } ); - const duration = Date.now() - startTime; - logger.debug(`__task__ __db__ Update Host stats ✔️ (${duration}ms)`); - return data }, - // Stacks: - // This is the stack config which will be saved in the database, the "real" docker-compose can be found in the designated folder addStack(stack_config: stacks_config) { - const startTime = Date.now(); - logger.debug("__task__ __db__ Add Stack config ⏳") - - const stmt = db.prepare(` - INSERT INTO stacks_config ( - name, - version, - custom, - source, - container_count, - stack_prefix, - automatic_reboot_on_error, - image_updates - ) - VALUES(?, ?, ?, ?, ?, ?, ?, ?); - `); - - const data = stmt.run( - stack_config.name, - stack_config.version, - stack_config.custom, - stack_config.source, - stack_config.container_count, - stack_config.stack_prefix, - stack_config.automatic_reboot_on_error, - stack_config.image_updates + return executeDbOperation( + "Add Stack Config", + () => { + const stmt = db.prepare(` + INSERT INTO stacks_config ( + name, + version, + custom, + source, + container_count, + stack_prefix, + automatic_reboot_on_error, + image_updates + ) + VALUES(?, ?, ?, ?, ?, ?, ?, ?) + `); + const data = stmt.run( + stack_config.name, + stack_config.version, + stack_config.custom, + stack_config.source, + stack_config.container_count, + stack_config.stack_prefix, + stack_config.automatic_reboot_on_error, + stack_config.image_updates + ); + relayController.stackAdded(); + return data; + }, + () => { } ); - - const duration = Date.now() - startTime; - logger.debug(`__task__ __db__ Add Stack config ✔️ (${duration}ms)`); - relayController.stackAdded(); - return data; }, getStacks() { - const startTime = Date.now(); - logger.debug("__task__ __db__ Get Stack config ⏳") - - const stmt = db.prepare(` + return executeDbOperation( + "Get Stacks", + () => { + const stmt = db.prepare(` SELECT name, version, custom, source, container_count, stack_prefix, automatic_reboot_on_error, image_updates FROM stacks_config ORDER BY name DESC `); - const data = stmt.all(); - - const duration = Date.now() - startTime; - logger.debug(`__task__ __db__ Get Stack config ✔️ (${duration}ms)`); - return data; + const data = stmt.all(); + return data; + }, + () => { } + ); }, deleteStack(name: string) { - const startTime = Date.now(); - logger.debug("__task__ __db__ Delete Stack config ⏳"); - - const stmt = db.prepare(` - DELETE FROM stacks_config - WHERE name = ?; - `); - const data = stmt.run(name); - - const duration = Date.now() - startTime; - logger.debug(`__task__ __db__ Delete Stack config ✔️ (${duration}ms)`); - relayController.stackDeleted(); - return data; + return executeDbOperation( + "Delete Stack", + () => { + const stmt = db.prepare(` + DELETE FROM stacks_config + WHERE name = ?; + `); + const data = stmt.run(name); + relayController.stackDeleted(); + return data; + }, + () => { } + ); }, updateStack(stack_config: stacks_config) { - const startTime = Date.now(); - logger.debug("__task__ __db__ Update Stack config ⏳"); - - const stmt = db.prepare(` - UPDATE stacks_config - SET - version = ?, - custom = ?, - source = ?, - container_count = ?, - stack_prefix = ?, - automatic_reboot_on_error = ?, - image_updates = ? - WHERE name = ?; - `); - const data = stmt.run( - stack_config.version, - stack_config.custom, - stack_config.source, - stack_config.container_count, - stack_config.stack_prefix, - stack_config.automatic_reboot_on_error, - stack_config.image_updates, - stack_config.name + return executeDbOperation( + "Update Stack", + () => { + const stmt = db.prepare(` + UPDATE stacks_config + SET + version = ?, + custom = ?, + source = ?, + container_count = ?, + stack_prefix = ?, + automatic_reboot_on_error = ?, + image_updates = ? + WHERE name = ?; + `); + const data = stmt.run( + stack_config.version, + stack_config.custom, + stack_config.source, + stack_config.container_count, + stack_config.stack_prefix, + stack_config.automatic_reboot_on_error, + stack_config.image_updates, + stack_config.name + ); + relayController.stackUpdated(); + return data; + }, + () => { } ); - - const duration = Date.now() - startTime; - logger.debug(`__task__ __db__ Update Stack config ✔️ (${duration}ms)`); - relayController.stackUpdated(); - return data; } -}; \ No newline at end of file +}; diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts index 82dc789..b35aeea 100644 --- a/src/core/utils/logger.ts +++ b/src/core/utils/logger.ts @@ -54,6 +54,7 @@ export const logger = createLogger({ format.timestamp({ format: "DD/MM HH:mm:ss" }), fileLineFormat(), format.printf(({ timestamp, level, message, file, line }) => { + const levelColors: Record = { error: chalk.red.bold, warn: chalk.yellow.bold, @@ -77,6 +78,19 @@ export const logger = createLogger({ const coloredLevel = (levelColors[level] || chalk.white)(paddedLevel); const coloredContext = chalk.cyan(`${file as string}:${line as number}`); const coloredTimestamp = chalk.yellow(timestamp); + + if (process.env.NODE_ENV !== "dev") { + return `${coloredLevel} [ ${coloredTimestamp} ] - ${chalk.gray( + message + )} - [ ${coloredContext} ]`; + } + + const prefix = `${paddedLevel} [ ${timestamp} ] - `; + const prefixLength = prefix.length; + const formattedMessage = formatTerminalMessage( + message as string, + prefixLength + ); const ansiRegex = /\x1B\[[0-?9;]*[mG]/g; try { @@ -89,22 +103,9 @@ export const logger = createLogger({ } catch (error) { // Use console.error to avoid recursive logging console.error(`Error inserting log into DB: ${String(error)}`); - console.error("Aborting due to risk of recursion!") process.abort() } - if (process.env.NODE_ENV !== "dev") { - return `${coloredLevel} [ ${coloredTimestamp} ] - ${chalk.gray( - message - )} - [ ${coloredContext} ]`; - } - - const prefix = `${paddedLevel} [ ${timestamp} ] - `; - const prefixLength = prefix.length; - const formattedMessage = formatTerminalMessage( - message as string, - prefixLength - ); return `${coloredLevel} [ ${coloredTimestamp} ] - ${formattedMessage} - [ ${coloredContext} ]`; }) ), From 91393d7c88a83f2ffefdbab2d0497d4d90e9cb76 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 12 Mar 2025 20:06:03 +0100 Subject: [PATCH 163/324] Fix: Robust parsing of Docker host URL. --- src/core/docker/client.ts | 52 ++++++++++++++++++++++++++++----------- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/src/core/docker/client.ts b/src/core/docker/client.ts index 3d5baff..ee0d714 100644 --- a/src/core/docker/client.ts +++ b/src/core/docker/client.ts @@ -2,36 +2,60 @@ import type { DockerHost } from "~/typings/docker"; import Docker from "dockerode"; import { logger } from "~/core/utils/logger"; +async function fileExists(path: string): Promise { + try { + return await Bun.file(path).exists(); + } catch (error) { + return false; + } +} + export const getDockerClient = (host: DockerHost): Docker => { try { - const [hostAddress, port] = host.url.split(":"); - const protocol = host.secure ? "https" : "http"; + const inputUrl = host.url.includes("://") ? host.url : `${host.secure ? "https" : "http"}://${host.url}`; + const parsedUrl = new URL(inputUrl); + const hostAddress = parsedUrl.hostname; + let port = parsedUrl.port ? parseInt(parsedUrl.port) : (host.secure ? 2376 : 2375); + + if (isNaN(port) || port < 1 || port > 65535) { + throw new Error("Invalid port number in Docker host URL"); + } + return new Docker({ - protocol, + protocol: host.secure ? "https" : "http", host: hostAddress, - port: port ? parseInt(port) : host.secure ? 2376 : 2375, + port, version: "v1.41", // TODO: Add TLS configuration if needed }); } catch (error) { - logger.error("Invalid Docker host URL configuration,", error); + logger.error("Invalid Docker host URL configuration:", error); throw new Error("Invalid Docker host configuration"); } }; -export const stackClient = (): Docker => { +export const stackClient = async (): Promise => { + const socketPath = "/var/run/docker.sock"; try { - const docker = new Docker({ - socketPath: "/var/run/docker.sock" - }) + if (!(await fileExists(socketPath))) { + throw new Error("Docker socket not found at " + socketPath); + } - docker.ping().catch(() => { - throw new Error("Could not ping local Docker-Socket") + const docker = new Docker({ + socketPath }); + const pingTimeout = 2000; + await Promise.race([ + docker.ping(), + new Promise((_, reject) => + setTimeout(() => reject(new Error("Ping timed out")), pingTimeout) + ) + ]); + return docker; } catch (error) { - logger.error(`Could not create Docker client for "/var/run/docker.sock" - ${error as string}`) - throw new Error() + logger.error(`Could not create Docker client for "${socketPath}" - ${error}`); + throw new Error("Failed to create Docker client for local Docker socket"); } -} \ No newline at end of file +}; From 583f5a90d2c63faeab725ddd50b212eab4cf6490 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 12 Mar 2025 20:13:49 +0100 Subject: [PATCH 164/324] Fix: Fixes #36 --- src/routes/docker-websocket.ts | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/routes/docker-websocket.ts b/src/routes/docker-websocket.ts index e040645..43c8038 100644 --- a/src/routes/docker-websocket.ts +++ b/src/routes/docker-websocket.ts @@ -12,9 +12,14 @@ import { responseHandler } from "~/core/utils/respone-handler"; import type { DockerHost } from "~/typings/docker"; import split2 from "split2"; import type { Readable } from "stream"; -import type internal from "stream"; import type { streams } from "~/typings/websocket"; +interface ExtendedWebSocket extends WebSocket { + isOpen: boolean; + streams: any[]; + heartbeat: NodeJS.Timeout | null; +} + const set: { headers: HTTPHeaders; status?: number | keyof StatusMap } = { headers: {}, }; @@ -26,10 +31,9 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( socket.send(JSON.stringify({ message: "Connection established" })); let hosts: DockerHost[]; - // Track if the WebSocket is open - (socket as any).isOpen = true; - (socket as any).streams = []; - (socket as any).heartbeat = null; // Add heartbeat reference + (socket as unknown as ExtendedWebSocket).isOpen = true; + (socket as unknown as ExtendedWebSocket).streams = []; + (socket as unknown as ExtendedWebSocket).heartbeat = null; // Add heartbeat reference logger.info(`Opened WebSocket (${socket.id})`); @@ -54,7 +58,7 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( // Add heartbeat using WebSocket protocol-level ping (socket as any).heartbeat = setInterval(() => { - if (!(socket as any).isOpen) { + if (!(socket as unknown as ExtendedWebSocket).isOpen) { clearInterval((socket as any).heartbeat); return; } @@ -62,7 +66,7 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( }, 30000); for (const host of hosts) { - if (!(socket as any).isOpen) { + if (!(socket as unknown as ExtendedWebSocket).isOpen) { break }; @@ -79,7 +83,7 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( ); for (const containerInfo of containers) { - if (!(socket as any).isOpen) { + if (!(socket as unknown as ExtendedWebSocket).isOpen) { break }; @@ -97,7 +101,7 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( const splitStream = split2(); // Store both streams for cleanup - (socket as any).streams.push({ statsStream, splitStream }); + (socket as unknown as ExtendedWebSocket).streams.push({ statsStream, splitStream }); // Handle stream lifecycle statsStream @@ -195,7 +199,7 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( } }, - message(socket, message) { + message(_, message) { if (message === "pong") { return }; @@ -203,14 +207,14 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( close(socket, code, reason) { logger.info(`Closing SplitStream and WebSocket (${socket.id})`); - const wasOpen = (socket as any).isOpen; - (socket as any).isOpen = false; + const wasOpen = (socket as unknown as ExtendedWebSocket).isOpen; + (socket as unknown as ExtendedWebSocket).isOpen = false; // Immediate heartbeat cleanup clearInterval((socket as any).heartbeat); // Force-close streams using destructor pattern - const streams: streams[] = (socket as any).streams || []; + const streams: streams[] = (socket as unknown as ExtendedWebSocket).streams || []; streams.forEach(({ statsStream, splitStream }) => { try { // Immediate pipeline breakdown From bacd092a836adbc8af20f08dfdbfc600ca68fb0f Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 13 Mar 2025 14:49:40 +0100 Subject: [PATCH 165/324] Feat: Server timing - won't do OpenTelementry - Closes: #41 --- bun.lock | 3 + package.json | 1 + src/core/database/helper.ts | 26 +- src/core/database/repository.ts | 51 ++- src/core/docker/client.ts | 20 +- src/core/docker/relay-controller.ts | 14 +- src/index.ts | 19 +- src/routes/api-config.ts | 47 ++- src/routes/docker-websocket.ts | 28 +- src/routes/stacks.ts | 440 +++++++++---------- src/typings/docker-compose.ts | 628 +++++++++++++++------------- 11 files changed, 667 insertions(+), 610 deletions(-) diff --git a/bun.lock b/bun.lock index f91ae69..ba25a0f 100644 --- a/bun.lock +++ b/bun.lock @@ -4,6 +4,7 @@ "": { "name": "dockstatapi", "dependencies": { + "@elysiajs/server-timing": "^1.2.1", "@elysiajs/static": "^1.2.0", "@elysiajs/swagger": "^1.2.2", "chalk": "^5.4.1", @@ -34,6 +35,8 @@ "@dabh/diagnostics": ["@dabh/diagnostics@2.0.3", "", { "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA=="], + "@elysiajs/server-timing": ["@elysiajs/server-timing@1.2.1", "", { "peerDependencies": { "elysia": ">= 1.2.0" } }, "sha512-7i4xOSYRdgljxKg8fyyBPVtnwsjhvJBnJn4qpTiNXt6ElrW1V2FeV2rdhyw2AQagUknnfpbUXMeDLalPaDeaLQ=="], + "@elysiajs/static": ["@elysiajs/static@1.2.0", "", { "dependencies": { "node-cache": "^5.1.2" }, "peerDependencies": { "elysia": ">= 1.2.0" } }, "sha512-oLpAi8c+maPpA0XhhK3BELaIjIG+nXg/K9p8cFfW4q5ayRD59a3MOMOOGgpiXZkHJzLPWcouhhyyLAYtaANW4g=="], "@elysiajs/swagger": ["@elysiajs/swagger@1.2.2", "", { "dependencies": { "@scalar/themes": "^0.9.52", "@scalar/types": "^0.0.12", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.2.0" } }, "sha512-DG0PbX/wzQNQ6kIpFFPCvmkkWTIbNWDS7lVLv3Puy6ONklF14B4NnbDfpYjX1hdSYKeCqKBBOuenh6jKm8tbYA=="], diff --git a/package.json b/package.json index 0650947..d194b46 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "clean:lin": "node -e \"process.exit(process.platform !== 'win32' ? 0 : 1)\" && rm -f dockstatapi.db* && echo 'success'" }, "dependencies": { + "@elysiajs/server-timing": "^1.2.1", "@elysiajs/static": "^1.2.0", "@elysiajs/swagger": "^1.2.2", "chalk": "^5.4.1", diff --git a/src/core/database/helper.ts b/src/core/database/helper.ts index 3bea044..ec9c1a7 100644 --- a/src/core/database/helper.ts +++ b/src/core/database/helper.ts @@ -1,17 +1,17 @@ import { logger } from "../utils/logger"; export function executeDbOperation( - label: string, - operation: () => T, - validate?: () => void + label: string, + operation: () => T, + validate?: () => void, ): T { - const startTime = Date.now(); - logger.debug(`__task__ __db__ ${label} �3`); - if (validate) { - validate(); - } - const result = operation(); - const duration = Date.now() - startTime; - logger.debug(`__task__ __db__ ${label} �4f (${duration}ms)`); - return result; -} \ No newline at end of file + const startTime = Date.now(); + logger.debug(`__task__ __db__ ${label} �3`); + if (validate) { + validate(); + } + const result = operation(); + const duration = Date.now() - startTime; + logger.debug(`__task__ __db__ ${label} �4f (${duration}ms)`); + return result; +} diff --git a/src/core/database/repository.ts b/src/core/database/repository.ts index 56543cf..a2c01fb 100644 --- a/src/core/database/repository.ts +++ b/src/core/database/repository.ts @@ -74,7 +74,6 @@ export const dbFunctions = { logger.info("Starting server..."); - /* * Default values: * - Websocket polling interval 5 seconds @@ -106,7 +105,7 @@ export const dbFunctions = { ); stmt.run("Localhost", "localhost:2375", false); } - logger.debug("__task__ __db__ Initializing Database ⏳") + logger.debug("__task__ __db__ Initializing Database ⏳"); const duration = Date.now() - startTime; logger.debug(`__task__ __db__ Initializing Database ✔️ (${duration}ms)`); }, @@ -130,7 +129,7 @@ export const dbFunctions = { logger.error("Invalid parameter types for addDockerHost"); throw new TypeError("Invalid parameter types for addDockerHost"); } - } + }, ); }, @@ -146,7 +145,7 @@ export const dbFunctions = { const data = stmt.all(); return data as DockerHost[]; }, - () => { } + () => {}, ); }, @@ -185,7 +184,7 @@ export const dbFunctions = { const data = stmt.all(); return data; }, - () => { } + () => {}, ); }, @@ -207,7 +206,7 @@ export const dbFunctions = { logger.error("Level parameter must be a string"); throw new TypeError("Level parameter must be a string"); } - } + }, ); }, @@ -232,7 +231,7 @@ export const dbFunctions = { logger.error("Invalid parameter types for updateDockerHost"); throw new TypeError("Invalid parameter types for updateDockerHost"); } - } + }, ); }, @@ -252,7 +251,7 @@ export const dbFunctions = { logger.error("Invalid parameter type for deleteDockerHost"); throw new TypeError("Name parameter must be a string"); } - } + }, ); }, @@ -266,7 +265,7 @@ export const dbFunctions = { const data = stmt.run(); return data; }, - () => { } + () => {}, ); }, @@ -286,14 +285,14 @@ export const dbFunctions = { logger.error("Invalid parameter type for clearLogsByLevel"); throw new TypeError("Level parameter must be a string"); } - } + }, ); }, updateConfig( polling_rate: number, fetching_interval: number, - keep_data_for: number + keep_data_for: number, ) { return executeDbOperation( "Update Config", @@ -316,7 +315,7 @@ export const dbFunctions = { logger.error("Invalid parameter types for updateConfig"); throw new TypeError("Invalid parameter types for updateConfig"); } - } + }, ); }, @@ -331,7 +330,7 @@ export const dbFunctions = { const data = stmt.all(); return data; }, - () => { } + () => {}, ); }, @@ -356,7 +355,7 @@ export const dbFunctions = { logger.error("Invalid parameter type for deleteOldData"); throw new TypeError("Days parameter must be a number"); } - } + }, ); }, @@ -368,7 +367,7 @@ export const dbFunctions = { status: string, state: string, cpu_usage: number, - memory_usage: number + memory_usage: number, ) { return executeDbOperation( "Add Container Stats", @@ -385,7 +384,7 @@ export const dbFunctions = { status, state, cpu_usage, - memory_usage + memory_usage, ); return data; }, @@ -403,7 +402,7 @@ export const dbFunctions = { logger.error("Invalid parameter types for addContainerStats"); throw new TypeError("Invalid parameter types for addContainerStats"); } - } + }, ); }, @@ -456,11 +455,11 @@ export const dbFunctions = { stats.containersRunning, stats.containersStopped, stats.containersPaused, - stats.images + stats.images, ); return data; }, - () => { } + () => {}, ); }, @@ -489,12 +488,12 @@ export const dbFunctions = { stack_config.container_count, stack_config.stack_prefix, stack_config.automatic_reboot_on_error, - stack_config.image_updates + stack_config.image_updates, ); relayController.stackAdded(); return data; }, - () => { } + () => {}, ); }, @@ -510,7 +509,7 @@ export const dbFunctions = { const data = stmt.all(); return data; }, - () => { } + () => {}, ); }, @@ -526,7 +525,7 @@ export const dbFunctions = { relayController.stackDeleted(); return data; }, - () => { } + () => {}, ); }, @@ -554,12 +553,12 @@ export const dbFunctions = { stack_config.stack_prefix, stack_config.automatic_reboot_on_error, stack_config.image_updates, - stack_config.name + stack_config.name, ); relayController.stackUpdated(); return data; }, - () => { } + () => {}, ); - } + }, }; diff --git a/src/core/docker/client.ts b/src/core/docker/client.ts index ee0d714..b97ef13 100644 --- a/src/core/docker/client.ts +++ b/src/core/docker/client.ts @@ -12,10 +12,16 @@ async function fileExists(path: string): Promise { export const getDockerClient = (host: DockerHost): Docker => { try { - const inputUrl = host.url.includes("://") ? host.url : `${host.secure ? "https" : "http"}://${host.url}`; + const inputUrl = host.url.includes("://") + ? host.url + : `${host.secure ? "https" : "http"}://${host.url}`; const parsedUrl = new URL(inputUrl); const hostAddress = parsedUrl.hostname; - let port = parsedUrl.port ? parseInt(parsedUrl.port) : (host.secure ? 2376 : 2375); + let port = parsedUrl.port + ? parseInt(parsedUrl.port) + : host.secure + ? 2376 + : 2375; if (isNaN(port) || port < 1 || port > 65535) { throw new Error("Invalid port number in Docker host URL"); @@ -42,20 +48,22 @@ export const stackClient = async (): Promise => { } const docker = new Docker({ - socketPath + socketPath, }); const pingTimeout = 2000; await Promise.race([ docker.ping(), new Promise((_, reject) => - setTimeout(() => reject(new Error("Ping timed out")), pingTimeout) - ) + setTimeout(() => reject(new Error("Ping timed out")), pingTimeout), + ), ]); return docker; } catch (error) { - logger.error(`Could not create Docker client for "${socketPath}" - ${error}`); + logger.error( + `Could not create Docker client for "${socketPath}" - ${error}`, + ); throw new Error("Failed to create Docker client for local Docker socket"); } }; diff --git a/src/core/docker/relay-controller.ts b/src/core/docker/relay-controller.ts index db8b6bb..f99314d 100644 --- a/src/core/docker/relay-controller.ts +++ b/src/core/docker/relay-controller.ts @@ -1,13 +1,7 @@ // Import any function here, when any of the specifies functions is detected, it will run said function export const relayController = { - stackAdded() { - - }, - stackDeleted() { - - }, - stackUpdated() { - - } -} \ No newline at end of file + stackAdded() {}, + stackDeleted() {}, + stackUpdated() {}, +}; diff --git a/src/index.ts b/src/index.ts index a2dfb81..975ebf9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,9 +10,10 @@ import { dockerWebsocketRoutes } from "~/routes/docker-websocket"; import { stackRoutes } from "./routes/stacks"; import { apiConfigRoutes } from "~/routes/api-config"; import { setSchedules } from "~/core/docker/scheduler"; +import { serverTiming } from "@elysiajs/server-timing"; import staticPlugin from "@elysiajs/static"; -console.log("") +console.log(""); dbFunctions.init(); const DockStatAPI = new Elysia() @@ -47,6 +48,7 @@ const DockStatAPI = new Elysia() }, }), ) + .use(serverTiming()) .use(dockerRoutes) .use(dockerStatsRoutes) .use(backendLogs) @@ -55,11 +57,11 @@ const DockStatAPI = new Elysia() .use(stackRoutes) .get("/health", () => ({ status: "healthy" }), { tags: ["Utils"] }) .onError(({ code, set }) => { - if (code === 'NOT_FOUND') { - logger.warn("Unknown route, showing error page!") - set.status = 404 - set.headers['Content-Type'] = 'text/html' - return Bun.file('public/404.html') + if (code === "NOT_FOUND") { + logger.warn("Unknown route, showing error page!"); + set.status = 404; + set.headers["Content-Type"] = "text/html"; + return Bun.file("public/404.html"); } }); @@ -67,7 +69,7 @@ async function startServer() { try { await loadPlugins("./src/plugins"); DockStatAPI.listen(3000, ({ hostname, port }) => { - console.log("----- [ ############## ]") + console.log("----- [ ############## ]"); logger.info(`DockStatAPI is running at http://${hostname}:${port}`); logger.info( `Swagger API Documentation available at http://${hostname}:${port}/swagger`, @@ -83,5 +85,4 @@ await setSchedules(); await startServer(); logger.info("Started server"); -console.log("----- [ ############## ]") - +console.log("----- [ ############## ]"); diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index e618551..1cdda5e 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -67,29 +67,30 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) tags: ["Management"], }, ) - .get("/package", async ({ set }) => { - try { - logger.debug("Fetching package.json"); - return { - version: version, - description: description, - license: license, - authorName: authorName, - authorEmail: authorEmail, - authorWebsite: authorWebsite, - contributors: contributors, - dependencies: dependencies, - devDependencies: devDependencies, - }; - - } catch (error) { - return responseHandler.error( - set, - error as string, - "Error while reading package.json", - ); - } - }, + .get( + "/package", + async ({ set }) => { + try { + logger.debug("Fetching package.json"); + return { + version: version, + description: description, + license: license, + authorName: authorName, + authorEmail: authorEmail, + authorWebsite: authorWebsite, + contributors: contributors, + dependencies: dependencies, + devDependencies: devDependencies, + }; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Error while reading package.json", + ); + } + }, { tags: ["Management"], }, diff --git a/src/routes/docker-websocket.ts b/src/routes/docker-websocket.ts index 43c8038..4b4fae7 100644 --- a/src/routes/docker-websocket.ts +++ b/src/routes/docker-websocket.ts @@ -67,8 +67,8 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( for (const host of hosts) { if (!(socket as unknown as ExtendedWebSocket).isOpen) { - break - }; + break; + } logger.debug(`Processing host: ${host.name}`); @@ -84,8 +84,8 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( for (const containerInfo of containers) { if (!(socket as unknown as ExtendedWebSocket).isOpen) { - break - }; + break; + } logger.debug( `Processing container ${containerInfo.Id} on host ${host.name}`, @@ -101,7 +101,10 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( const splitStream = split2(); // Store both streams for cleanup - (socket as unknown as ExtendedWebSocket).streams.push({ statsStream, splitStream }); + (socket as unknown as ExtendedWebSocket).streams.push({ + statsStream, + splitStream, + }); // Handle stream lifecycle statsStream @@ -119,11 +122,11 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( .on("data", (line: string) => { // 1 = OPEN state if (socket.readyState !== 1) { - return - }; + return; + } if (!line) { - return - }; + return; + } try { const stats = JSON.parse(line); const cpuUsage = calculateCpuPercent(stats); @@ -201,8 +204,8 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( message(_, message) { if (message === "pong") { - return - }; + return; + } }, close(socket, code, reason) { @@ -214,7 +217,8 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( clearInterval((socket as any).heartbeat); // Force-close streams using destructor pattern - const streams: streams[] = (socket as unknown as ExtendedWebSocket).streams || []; + const streams: streams[] = + (socket as unknown as ExtendedWebSocket).streams || []; streams.forEach(({ statsStream, splitStream }) => { try { // Immediate pipeline breakdown diff --git a/src/routes/stacks.ts b/src/routes/stacks.ts index 600dec5..3fcf911 100644 --- a/src/routes/stacks.ts +++ b/src/routes/stacks.ts @@ -1,239 +1,239 @@ import { Elysia, error, t } from "elysia"; import { responseHandler } from "~/core/utils/respone-handler"; import { - deployStack, - stopStack, - pullStackImages, - restartStack, - getStackStatus, - startStack + deployStack, + stopStack, + pullStackImages, + restartStack, + getStackStatus, + startStack, } from "~/core/stacks/controller"; import { dbFunctions } from "~/core/database/repository"; import { logger } from "~/core/utils/logger"; export const stackRoutes = new Elysia({ prefix: "/stacks" }) - .post( - "/deploy", - async ({ set, body }) => { - try { - const isCustom = body.isCustom || false; + .post( + "/deploy", + async ({ set, body }) => { + try { + const isCustom = body.isCustom || false; + const image_updates = body.image_updates || false; - const image_updates = body.image_updates || false; - - - let missingParams: string[] = []; - if (!body.compose_spec) { - missingParams.push("compose_spec"); - } - if (!body.automatic_reboot_on_error) { - missingParams.push("automatic_reboot_on_error"); - } - if (!body.source) { - missingParams.push("source"); - } - if (!body.name) { - missingParams.push("name"); - } - - if (missingParams.length > 0) { - const errMsg = `Missing values of: ${missingParams.join("; ")}`; - return responseHandler.error(set, errMsg, errMsg); - } - - await deployStack( - body.compose_spec, - body.name, - body.version, - body.source, - body.automatic_reboot_on_error, - isCustom, - image_updates, - body.stack_prefix - ); - logger.info(`Deployed Stack (${body.name})`) - return responseHandler.ok( - set, - `Stack ${body.name} deployed successfully` - ); - } catch (error: any) { - return responseHandler.error( - set, - error.message || error, - "Error deploying stack" - ); - } - }, - { - detail: { tags: ["Stacks"] }, - body: t.Object({ - compose_spec: t.Any(), - name: t.String(), - version: t.Number(), - automatic_reboot_on_error: t.Boolean(), - isCustom: t.Boolean(), - image_updates: t.Boolean(), - source: t.String(), - stack_prefix: t.Optional(t.String()), - }), + let missingParams: string[] = []; + if (!body.compose_spec) { + missingParams.push("compose_spec"); } - ) - .post( - "/start", - async ({ set, body }) => { - try { - if (!body.stack) { - throw new Error("Stack needed") - } - await startStack(body.stack); - logger.info(`Started Stack (${body.stack})`) - return responseHandler.ok( - set, - `Stack ${body.stack} started successfully` - ); - } catch (error: any) { - return responseHandler.error( - set, - error.message || error, - "Error starting stack" - ); - } - }, - { - detail: { tags: ["Stacks"] }, - body: t.Object({ - stack: t.Any(), - }), + if (!body.automatic_reboot_on_error) { + missingParams.push("automatic_reboot_on_error"); } - ) - .post( - "/stop", - async ({ set, body }) => { - try { - if (!body.stack) { - throw new Error("Stack needed") - } - await stopStack(body.stack); - logger.info(`Stopped Stack (${body.stack})`) - return responseHandler.ok( - set, - `Stack ${body.stack} stopped successfully` - ); - } catch (error: any) { - return responseHandler.error( - set, - error.message || error, - "Error stopping stack" - ); - } - }, - { - detail: { tags: ["Stacks"] }, - body: t.Object({ - stack: t.Any(), - }), + if (!body.source) { + missingParams.push("source"); } - ) - .post( - "/restart", - async ({ set, body }) => { - try { - if (!body.stack) { - throw new Error("Stack needed") - } - await restartStack(body.stack); - logger.info(`Restarted Stack (${body.stack})`) - return responseHandler.ok( - set, - `Stack ${body.stack} restarted successfully` - ); - } catch (error: any) { - return responseHandler.error( - set, - error.message || error, - "Error restarting stack" - ); - } - }, - { - detail: { tags: ["Stacks"] }, - body: t.Object({ - stack: t.Any(), - }), + if (!body.name) { + missingParams.push("name"); } - ) - .post( - "/pull-images", - async ({ set, body }) => { - try { - if (!body.stack) { - throw new Error("Stack needed") - } - await pullStackImages(body.stack); - logger.info(`Pulled Stack images (${body.stack})`) - return responseHandler.ok( - set, - `Images for stack ${body.stack} pulled successfully` - ); - } catch (error: any) { - return responseHandler.error( - set, - error.message || error, - "Error pulling images" - ); - } - }, - { - detail: { tags: ["Stacks"] }, - body: t.Object({ - stack: t.Any(), - }), + + if (missingParams.length > 0) { + const errMsg = `Missing values of: ${missingParams.join("; ")}`; + return responseHandler.error(set, errMsg, errMsg); } - ) - .get( - "/status", - async ({ set, query }) => { - try { - if (!query.stack_name) { - throw new Error("Stack needed") - } - logger.debug(query.stack_name) - const status = await getStackStatus(query.stack_name); - const res = responseHandler.ok( - set, - `Stack ${query.stack_name} status retrieved successfully` - ); - logger.info("Fetched Stack status") - return { ...res, status: status }; - } catch (error: any) { - return responseHandler.error( - set, - error.message || error, - "Error getting stack status" - ); - } - }, - { - detail: { tags: ["Stacks"] }, - query: t.Object({ - stack_name: t.Any(), - }), + + await deployStack( + body.compose_spec, + body.name, + body.version, + body.source, + body.automatic_reboot_on_error, + isCustom, + image_updates, + body.stack_prefix, + ); + logger.info(`Deployed Stack (${body.name})`); + return responseHandler.ok( + set, + `Stack ${body.name} deployed successfully`, + ); + } catch (error: any) { + return responseHandler.error( + set, + error.message || error, + "Error deploying stack", + ); + } + }, + { + detail: { tags: ["Stacks"] }, + body: t.Object({ + compose_spec: t.Any(), + name: t.String(), + version: t.Number(), + automatic_reboot_on_error: t.Boolean(), + isCustom: t.Boolean(), + image_updates: t.Boolean(), + source: t.String(), + stack_prefix: t.Optional(t.String()), + }), + }, + ) + .post( + "/start", + async ({ set, body }) => { + try { + if (!body.stack) { + throw new Error("Stack needed"); } - ) - .get("/", async ({ set }) => { - try { - const stacks = dbFunctions.getStacks(); - logger.info("Fetched Stacks") - return stacks; - } catch (error: any) { - return responseHandler.error( - set, - error.message || error, - "Error getting stacks" - ); + await startStack(body.stack); + logger.info(`Started Stack (${body.stack})`); + return responseHandler.ok( + set, + `Stack ${body.stack} started successfully`, + ); + } catch (error: any) { + return responseHandler.error( + set, + error.message || error, + "Error starting stack", + ); + } + }, + { + detail: { tags: ["Stacks"] }, + body: t.Object({ + stack: t.Any(), + }), + }, + ) + .post( + "/stop", + async ({ set, body }) => { + try { + if (!body.stack) { + throw new Error("Stack needed"); } + await stopStack(body.stack); + logger.info(`Stopped Stack (${body.stack})`); + return responseHandler.ok( + set, + `Stack ${body.stack} stopped successfully`, + ); + } catch (error: any) { + return responseHandler.error( + set, + error.message || error, + "Error stopping stack", + ); + } + }, + { + detail: { tags: ["Stacks"] }, + body: t.Object({ + stack: t.Any(), + }), }, - { - detail: { tags: ["Stacks"] }, + ) + .post( + "/restart", + async ({ set, body }) => { + try { + if (!body.stack) { + throw new Error("Stack needed"); } - ); + await restartStack(body.stack); + logger.info(`Restarted Stack (${body.stack})`); + return responseHandler.ok( + set, + `Stack ${body.stack} restarted successfully`, + ); + } catch (error: any) { + return responseHandler.error( + set, + error.message || error, + "Error restarting stack", + ); + } + }, + { + detail: { tags: ["Stacks"] }, + body: t.Object({ + stack: t.Any(), + }), + }, + ) + .post( + "/pull-images", + async ({ set, body }) => { + try { + if (!body.stack) { + throw new Error("Stack needed"); + } + await pullStackImages(body.stack); + logger.info(`Pulled Stack images (${body.stack})`); + return responseHandler.ok( + set, + `Images for stack ${body.stack} pulled successfully`, + ); + } catch (error: any) { + return responseHandler.error( + set, + error.message || error, + "Error pulling images", + ); + } + }, + { + detail: { tags: ["Stacks"] }, + body: t.Object({ + stack: t.Any(), + }), + }, + ) + .get( + "/status", + async ({ set, query }) => { + try { + if (!query.stack_name) { + throw new Error("Stack needed"); + } + logger.debug(query.stack_name); + const status = await getStackStatus(query.stack_name); + const res = responseHandler.ok( + set, + `Stack ${query.stack_name} status retrieved successfully`, + ); + logger.info("Fetched Stack status"); + return { ...res, status: status }; + } catch (error: any) { + return responseHandler.error( + set, + error.message || error, + "Error getting stack status", + ); + } + }, + { + detail: { tags: ["Stacks"] }, + query: t.Object({ + stack_name: t.Any(), + }), + }, + ) + .get( + "/", + async ({ set }) => { + try { + const stacks = dbFunctions.getStacks(); + logger.info("Fetched Stacks"); + return stacks; + } catch (error: any) { + return responseHandler.error( + set, + error.message || error, + "Error getting stacks", + ); + } + }, + { + detail: { tags: ["Stacks"] }, + }, + ); diff --git a/src/typings/docker-compose.ts b/src/typings/docker-compose.ts index a554c21..9b23c2e 100644 --- a/src/typings/docker-compose.ts +++ b/src/typings/docker-compose.ts @@ -1,30 +1,38 @@ export interface Stack { - compose_spec: ComposeSpec; - name: string - version: number; - source: string; + compose_spec: ComposeSpec; + name: string; + version: number; + source: string; } export interface ComposeSpec { - version?: string; - name?: string; - include?: Include[]; - services?: { [key: string]: Service }; - networks?: { [key: string]: Network }; - volumes?: { [key: string]: Volume }; - secrets?: { [key: string]: Secret }; - configs?: { [key: string]: Config }; - [key: `x-${string}`]: any; + version?: string; + name?: string; + include?: Include[]; + services?: { [key: string]: Service }; + networks?: { [key: string]: Network }; + volumes?: { [key: string]: Volume }; + secrets?: { [key: string]: Secret }; + configs?: { [key: string]: Config }; + [key: `x-${string}`]: any; } -type Include = string | { path: string | string[]; env_file?: string | string[]; project_directory?: string }; +type Include = + | string + | { + path: string | string[]; + env_file?: string | string[]; + project_directory?: string; + }; interface Service { - develop?: Development | null; - deploy?: Deployment | null; - annotations?: ListOrDict; - attach?: boolean | string; - build?: string | { + develop?: Development | null; + deploy?: Deployment | null; + annotations?: ListOrDict; + attach?: boolean | string; + build?: + | string + | { context?: string; dockerfile?: string; dockerfile_inline?: string; @@ -48,110 +56,125 @@ interface Service { ulimits?: Ulimits; platforms?: string[]; [key: `x-${string}`]: any; - }; - blkio_config?: { - device_read_bps?: BlkioLimit[]; - device_read_iops?: BlkioLimit[]; - device_write_bps?: BlkioLimit[]; - device_write_iops?: BlkioLimit[]; - weight?: number | string; - weight_device?: BlkioWeight[]; - }; - cap_add?: string[]; - cap_drop?: string[]; - cgroup?: 'host' | 'private'; - cgroup_parent?: string; - command?: Command; - configs?: ServiceConfigOrSecret[]; - container_name?: string; - cpu_count?: string | number; - cpu_percent?: string | number; - cpu_shares?: number | string; - cpu_quota?: number | string; - cpu_period?: number | string; - cpu_rt_period?: number | string; - cpu_rt_runtime?: number | string; - cpus?: number | string; - cpuset?: string; - credential_spec?: { - config?: string; - file?: string; - registry?: string; - [key: `x-${string}`]: any; - }; - depends_on?: string[] | { + }; + blkio_config?: { + device_read_bps?: BlkioLimit[]; + device_read_iops?: BlkioLimit[]; + device_write_bps?: BlkioLimit[]; + device_write_iops?: BlkioLimit[]; + weight?: number | string; + weight_device?: BlkioWeight[]; + }; + cap_add?: string[]; + cap_drop?: string[]; + cgroup?: "host" | "private"; + cgroup_parent?: string; + command?: Command; + configs?: ServiceConfigOrSecret[]; + container_name?: string; + cpu_count?: string | number; + cpu_percent?: string | number; + cpu_shares?: number | string; + cpu_quota?: number | string; + cpu_period?: number | string; + cpu_rt_period?: number | string; + cpu_rt_runtime?: number | string; + cpus?: number | string; + cpuset?: string; + credential_spec?: { + config?: string; + file?: string; + registry?: string; + [key: `x-${string}`]: any; + }; + depends_on?: + | string[] + | { [service: string]: { - condition: 'service_started' | 'service_healthy' | 'service_completed_successfully'; - restart?: boolean | string; - required?: boolean; - [key: `x-${string}`]: any; - } - }; - device_cgroup_rules?: string[]; - devices?: (string | { + condition: + | "service_started" + | "service_healthy" + | "service_completed_successfully"; + restart?: boolean | string; + required?: boolean; + [key: `x-${string}`]: any; + }; + }; + device_cgroup_rules?: string[]; + devices?: ( + | string + | { source: string; target?: string; permissions?: string; [key: `x-${string}`]: any; - })[]; - dns?: StringOrList; - dns_opt?: string[]; - dns_search?: StringOrList; - domainname?: string; - entrypoint?: Command; - env_file?: EnvFile; - label_file?: string | string[]; - environment?: ListOrDict; - expose?: (string | number)[]; - extends?: string | { service: string; file?: string }; - external_links?: string[]; - extra_hosts?: ExtraHosts; - gpus?: 'all' | Array<{ + } + )[]; + dns?: StringOrList; + dns_opt?: string[]; + dns_search?: StringOrList; + domainname?: string; + entrypoint?: Command; + env_file?: EnvFile; + label_file?: string | string[]; + environment?: ListOrDict; + expose?: (string | number)[]; + extends?: string | { service: string; file?: string }; + external_links?: string[]; + extra_hosts?: ExtraHosts; + gpus?: + | "all" + | Array<{ capabilities?: string[]; count?: string | number; device_ids?: string[]; driver?: string; options?: ListOrDict; [key: `x-${string}`]: any; - }>; - group_add?: (string | number)[]; - healthcheck?: Healthcheck; - hostname?: string; - image?: string; - init?: boolean | string; - ipc?: string; - isolation?: string; - labels?: ListOrDict; - links?: string[]; - logging?: { - driver?: string; - options?: { [key: string]: string | number | null }; - [key: `x-${string}`]: any; - }; - mac_address?: string; - mem_limit?: number | string; - mem_reservation?: string | number; - mem_swappiness?: number | string; - memswap_limit?: number | string; - network_mode?: string; - networks?: string[] | { + }>; + group_add?: (string | number)[]; + healthcheck?: Healthcheck; + hostname?: string; + image?: string; + init?: boolean | string; + ipc?: string; + isolation?: string; + labels?: ListOrDict; + links?: string[]; + logging?: { + driver?: string; + options?: { [key: string]: string | number | null }; + [key: `x-${string}`]: any; + }; + mac_address?: string; + mem_limit?: number | string; + mem_reservation?: string | number; + mem_swappiness?: number | string; + memswap_limit?: number | string; + network_mode?: string; + networks?: + | string[] + | { [network: string]: { - aliases?: string[]; - ipv4_address?: string; - ipv6_address?: string; - link_local_ips?: string[]; - mac_address?: string; - driver_opts?: { [key: string]: string | number }; - priority?: number; - [key: `x-${string}`]: any; + aliases?: string[]; + ipv4_address?: string; + ipv6_address?: string; + link_local_ips?: string[]; + mac_address?: string; + driver_opts?: { [key: string]: string | number }; + priority?: number; + [key: `x-${string}`]: any; } | null; - }; - oom_kill_disable?: boolean | string; - oom_score_adj?: string | number; - pid?: string | null; - pids_limit?: number | string; - platform?: string; - ports?: (number | string | { + }; + oom_kill_disable?: boolean | string; + oom_score_adj?: string | number; + pid?: string | null; + pids_limit?: number | string; + platform?: string; + ports?: ( + | number + | string + | { name?: string; mode?: string; host_ip?: string; @@ -160,234 +183,257 @@ interface Service { protocol?: string; app_protocol?: string; [key: `x-${string}`]: any; - })[]; - post_start?: ServiceHook[]; - pre_stop?: ServiceHook[]; - privileged?: boolean | string; - profiles?: string[]; - pull_policy?: 'always' | 'never' | 'if_not_present' | 'build' | 'missing'; - read_only?: boolean | string; - restart?: string; - runtime?: string; - scale?: number | string; - security_opt?: string[]; - shm_size?: number | string; - secrets?: ServiceConfigOrSecret[]; - sysctls?: ListOrDict; - stdin_open?: boolean | string; - stop_grace_period?: string; - stop_signal?: string; - storage_opt?: object; - tmpfs?: StringOrList; - tty?: boolean | string; - ulimits?: Ulimits; - user?: string; - uts?: string; - userns_mode?: string; - volumes?: (string | { + } + )[]; + post_start?: ServiceHook[]; + pre_stop?: ServiceHook[]; + privileged?: boolean | string; + profiles?: string[]; + pull_policy?: "always" | "never" | "if_not_present" | "build" | "missing"; + read_only?: boolean | string; + restart?: string; + runtime?: string; + scale?: number | string; + security_opt?: string[]; + shm_size?: number | string; + secrets?: ServiceConfigOrSecret[]; + sysctls?: ListOrDict; + stdin_open?: boolean | string; + stop_grace_period?: string; + stop_signal?: string; + storage_opt?: object; + tmpfs?: StringOrList; + tty?: boolean | string; + ulimits?: Ulimits; + user?: string; + uts?: string; + userns_mode?: string; + volumes?: ( + | string + | { type: string; source?: string; target?: string; read_only?: boolean | string; consistency?: string; bind?: { - propagation?: string; - create_host_path?: boolean | string; - recursive?: 'enabled' | 'disabled' | 'writable' | 'readonly'; - selinux?: 'z' | 'Z'; - [key: `x-${string}`]: any; + propagation?: string; + create_host_path?: boolean | string; + recursive?: "enabled" | "disabled" | "writable" | "readonly"; + selinux?: "z" | "Z"; + [key: `x-${string}`]: any; }; volume?: { - nocopy?: boolean | string; - subpath?: string; - [key: `x-${string}`]: any; + nocopy?: boolean | string; + subpath?: string; + [key: `x-${string}`]: any; }; tmpfs?: { - size?: number | string; - mode?: number | string; - [key: `x-${string}`]: any; + size?: number | string; + mode?: number | string; + [key: `x-${string}`]: any; }; [key: `x-${string}`]: any; - })[]; - volumes_from?: string[]; - working_dir?: string; - [key: `x-${string}`]: any; + } + )[]; + volumes_from?: string[]; + working_dir?: string; + [key: `x-${string}`]: any; } interface Healthcheck { - disable?: boolean | string; - interval?: string; - retries?: number | string; - test?: string | string[]; - timeout?: string; - start_period?: string; - start_interval?: string; - [key: `x-${string}`]: any; + disable?: boolean | string; + interval?: string; + retries?: number | string; + test?: string | string[]; + timeout?: string; + start_period?: string; + start_interval?: string; + [key: `x-${string}`]: any; } interface Development { - watch?: Array<{ - path: string; - action: 'rebuild' | 'sync' | 'restart' | 'sync+restart' | 'sync+exec'; - ignore?: string[]; - target?: string; - exec?: ServiceHook; - [key: `x-${string}`]: any; - }>; + watch?: Array<{ + path: string; + action: "rebuild" | "sync" | "restart" | "sync+restart" | "sync+exec"; + ignore?: string[]; + target?: string; + exec?: ServiceHook; [key: `x-${string}`]: any; + }>; + [key: `x-${string}`]: any; } interface Deployment { - mode?: string; - endpoint_mode?: string; - replicas?: number | string; - labels?: ListOrDict; - rollback_config?: { - parallelism?: number | string; - delay?: string; - failure_action?: string; - monitor?: string; - max_failure_ratio?: number | string; - order?: 'start-first' | 'stop-first'; - [key: `x-${string}`]: any; - }; - update_config?: { - parallelism?: number | string; - delay?: string; - failure_action?: string; - monitor?: string; - max_failure_ratio?: number | string; - order?: 'start-first' | 'stop-first'; - [key: `x-${string}`]: any; + mode?: string; + endpoint_mode?: string; + replicas?: number | string; + labels?: ListOrDict; + rollback_config?: { + parallelism?: number | string; + delay?: string; + failure_action?: string; + monitor?: string; + max_failure_ratio?: number | string; + order?: "start-first" | "stop-first"; + [key: `x-${string}`]: any; + }; + update_config?: { + parallelism?: number | string; + delay?: string; + failure_action?: string; + monitor?: string; + max_failure_ratio?: number | string; + order?: "start-first" | "stop-first"; + [key: `x-${string}`]: any; + }; + resources?: { + limits?: { + cpus?: number | string; + memory?: string; + pids?: number | string; + [key: `x-${string}`]: any; }; - resources?: { - limits?: { - cpus?: number | string; - memory?: string; - pids?: number | string; - [key: `x-${string}`]: any; - }; - reservations?: { - cpus?: number | string; - memory?: string; - generic_resources?: Array<{ - discrete_resource_spec?: { - kind?: string; - value?: number | string; - [key: `x-${string}`]: any; - }; - [key: `x-${string}`]: any; - }>; - devices?: Array<{ - capabilities?: string[]; - count?: string | number; - device_ids?: string[]; - driver?: string; - options?: ListOrDict; - [key: `x-${string}`]: any; - }>; - [key: `x-${string}`]: any; + reservations?: { + cpus?: number | string; + memory?: string; + generic_resources?: Array<{ + discrete_resource_spec?: { + kind?: string; + value?: number | string; + [key: `x-${string}`]: any; }; [key: `x-${string}`]: any; - }; - restart_policy?: { - condition?: string; - delay?: string; - max_attempts?: number | string; - window?: string; - [key: `x-${string}`]: any; - }; - placement?: { - constraints?: string[]; - preferences?: Array<{ - spread?: string; - [key: `x-${string}`]: any; - }>; - max_replicas_per_node?: number | string; + }>; + devices?: Array<{ + capabilities?: string[]; + count?: string | number; + device_ids?: string[]; + driver?: string; + options?: ListOrDict; [key: `x-${string}`]: any; + }>; + [key: `x-${string}`]: any; }; [key: `x-${string}`]: any; + }; + restart_policy?: { + condition?: string; + delay?: string; + max_attempts?: number | string; + window?: string; + [key: `x-${string}`]: any; + }; + placement?: { + constraints?: string[]; + preferences?: Array<{ + spread?: string; + [key: `x-${string}`]: any; + }>; + max_replicas_per_node?: number | string; + [key: `x-${string}`]: any; + }; + [key: `x-${string}`]: any; } type Command = string | string[] | null; -type EnvFile = string | Array; +type EnvFile = + | string + | Array< + string | { path: string; format?: string; required?: boolean | string } + >; type StringOrList = string | string[]; -type ListOrDict = { [key: string]: string | number | boolean | null } | string[]; +type ListOrDict = + | { [key: string]: string | number | boolean | null } + | string[]; type ExtraHosts = { [host: string]: string | string[] } | string[]; -interface BlkioLimit { path: string; rate: number | string; } -interface BlkioWeight { path: string; weight: number | string; } -type ServiceConfigOrSecret = string | { - source: string; - target?: string; - uid?: string; - gid?: string; - mode?: number | string; - [key: `x-${string}`]: any; +interface BlkioLimit { + path: string; + rate: number | string; +} +interface BlkioWeight { + path: string; + weight: number | string; +} +type ServiceConfigOrSecret = + | string + | { + source: string; + target?: string; + uid?: string; + gid?: string; + mode?: number | string; + [key: `x-${string}`]: any; + }; +type Ulimits = { + [key: string]: + | number + | string + | { hard: number | string; soft: number | string }; }; -type Ulimits = { [key: string]: number | string | { hard: number | string; soft: number | string } }; interface ServiceHook { - command?: Command; - user?: string; - privileged?: boolean | string; - working_dir?: string; - environment?: ListOrDict; - [key: `x-${string}`]: any; + command?: Command; + user?: string; + privileged?: boolean | string; + working_dir?: string; + environment?: ListOrDict; + [key: `x-${string}`]: any; } interface Network { - name?: string; + name?: string; + driver?: string; + driver_opts?: { [key: string]: string | number }; + ipam?: { driver?: string; - driver_opts?: { [key: string]: string | number }; - ipam?: { - driver?: string; - config?: Array<{ - subnet?: string; - ip_range?: string; - gateway?: string; - aux_addresses?: { [key: string]: string }; - [key: `x-${string}`]: any; - }>; - options?: { [key: string]: string }; - [key: `x-${string}`]: any; - }; - external?: boolean | string | { name?: string;[key: `x-${string}`]: any }; - internal?: boolean | string; - enable_ipv4?: boolean | string; - enable_ipv6?: boolean | string; - attachable?: boolean | string; - labels?: ListOrDict; + config?: Array<{ + subnet?: string; + ip_range?: string; + gateway?: string; + aux_addresses?: { [key: string]: string }; + [key: `x-${string}`]: any; + }>; + options?: { [key: string]: string }; [key: `x-${string}`]: any; + }; + external?: boolean | string | { name?: string; [key: `x-${string}`]: any }; + internal?: boolean | string; + enable_ipv4?: boolean | string; + enable_ipv6?: boolean | string; + attachable?: boolean | string; + labels?: ListOrDict; + [key: `x-${string}`]: any; } interface Volume { - name?: string; - driver?: string; - driver_opts?: { [key: string]: string | number }; - external?: boolean | string | { name?: string;[key: `x-${string}`]: any }; - labels?: ListOrDict; - [key: `x-${string}`]: any; + name?: string; + driver?: string; + driver_opts?: { [key: string]: string | number }; + external?: boolean | string | { name?: string; [key: `x-${string}`]: any }; + labels?: ListOrDict; + [key: `x-${string}`]: any; } interface Secret { - name?: string; - environment?: string; - file?: string; - external?: boolean | string | { name?: string;[key: string]: any }; - labels?: ListOrDict; - driver?: string; - driver_opts?: { [key: string]: string | number }; - template_driver?: string; - [key: `x-${string}`]: any; + name?: string; + environment?: string; + file?: string; + external?: boolean | string | { name?: string; [key: string]: any }; + labels?: ListOrDict; + driver?: string; + driver_opts?: { [key: string]: string | number }; + template_driver?: string; + [key: `x-${string}`]: any; } interface Config { - name?: string; - content?: string; - environment?: string; - file?: string; - external?: boolean | string | { name?: string;[key: string]: any }; - labels?: ListOrDict; - template_driver?: string; - [key: `x-${string}`]: any; -} \ No newline at end of file + name?: string; + content?: string; + environment?: string; + file?: string; + external?: boolean | string | { name?: string; [key: string]: any }; + labels?: ListOrDict; + template_driver?: string; + [key: `x-${string}`]: any; +} From d3219e582a99155df1f3d4a647b0938642a39ba0 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 13 Mar 2025 22:40:05 +0100 Subject: [PATCH 166/324] Feat: tRPC, ReadMe and Docs Update --- .github/DockStat.png | Bin 0 -> 79885 bytes .gitignore | 3 +- .knip.json | 4 + README.md | 85 ++------ bun.lock | 137 +++++++++++- package.json | 12 +- src/core/database/helper.ts | 6 +- src/core/database/repository.ts | 72 +++---- src/core/docker/client.ts | 40 +--- src/core/docker/relay-controller.ts | 7 - src/core/plugins/plugin-actions.ts | 3 - src/core/plugins/plugin-manager.ts | 8 +- src/core/trpc/README.md | 1 + src/core/trpc/index.ts | 4 + .../trpc/procedures/api-config.procedure.ts | 79 +++++++ .../procedures/docker-manager.procedure.ts | 65 ++++++ .../trpc/procedures/docker-stats.procedure.ts | 147 +++++++++++++ src/core/trpc/procedures/logs.procedure.ts | 73 +++++++ src/core/trpc/procedures/stacks.procedure.ts | 199 ++++++++++++++++++ src/core/trpc/router.ts | 21 ++ src/core/trpc/trpc.ts | 5 + src/index.ts | 9 +- src/plugins/example.plugin.ts | 1 - src/routes/api-config.ts | 21 +- src/routes/stacks.ts | 44 ++-- src/typings/database.ts | 2 +- 26 files changed, 841 insertions(+), 207 deletions(-) create mode 100644 .github/DockStat.png create mode 100644 .knip.json delete mode 100644 src/core/docker/relay-controller.ts create mode 100644 src/core/trpc/README.md create mode 100644 src/core/trpc/index.ts create mode 100644 src/core/trpc/procedures/api-config.procedure.ts create mode 100644 src/core/trpc/procedures/docker-manager.procedure.ts create mode 100644 src/core/trpc/procedures/docker-stats.procedure.ts create mode 100644 src/core/trpc/procedures/logs.procedure.ts create mode 100644 src/core/trpc/procedures/stacks.procedure.ts create mode 100644 src/core/trpc/router.ts create mode 100644 src/core/trpc/trpc.ts diff --git a/.github/DockStat.png b/.github/DockStat.png new file mode 100644 index 0000000000000000000000000000000000000000..d375bd49107c79a960488d6062276a72cf6bd512 GIT binary patch literal 79885 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⪼EggaS+_T%ej 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 KU+70|um2a&GMD85 literal 0 HcmV?d00001 diff --git a/.gitignore b/.gitignore index aa426ba..c61c683 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.db* /stacks -/node_modules \ No newline at end of file +/node_modules +.test \ No newline at end of file diff --git a/.knip.json b/.knip.json new file mode 100644 index 0000000..14f7f53 --- /dev/null +++ b/.knip.json @@ -0,0 +1,4 @@ +{ + "entry": ["src/index.ts"], + "project": ["src/**/*.ts"] +} \ No newline at end of file diff --git a/README.md b/README.md index cbb25b7..37881b7 100644 --- a/README.md +++ b/README.md @@ -1,72 +1,31 @@ -# DockStat API v3 +![Logo](.github/DockStat.png) +![CC BY-NC 4.0 License](https://img.shields.io/badge/License-CC_BY--NC_4.0-lightgrey.svg) -! WIP Documentation ! +--- -## Usage +# DockStatAPI -The DockStat API provides the following endpoints: +Docker monitoring API with real-time statistics, stack management, and plugin support. -### Docker Containers -- `GET /docker/containers`: Retrieve statistics for all containers across all configured Docker hosts. +## Features -### Docker Hosts -- `GET /docker/hosts/:id`: Retrieve configuration and statistics for a specific Docker host. +- Real-time container metrics via WebSocket +- Multi-host Docker environment monitoring +- Compose stack deployment/management +- Plugin system for custom logic/notifications +- Historical stats storage (SQLite) +- Swagger API documentation +- Web dashboard (WIP) -### Docker Configuration -- `POST /docker-config/add-host`: Add a new Docker host. -- `POST /docker-config/update-host`: Update an existing Docker host. -- `GET /docker-config/hosts`: Retrieve a list of all configured Docker hosts. +## Tech Stack -### API Configuration -- `GET /config/get`: Retrieve the current API configuration. -- `POST /config/update`: Update the API configuration. +- **Runtime**: [Bun.sh](http://Bun.sh) +- **Framework**: [Elysia.js](https://elysiajs.com/) +- **Database**: SQLite (WAL mode) +- **Docker**: dockerode + compose +- **Monitoring**: Custom metrics collection +- **Auth**: (TODO - Currently open) -### Logs -- `GET /logs`: Retrieve all backend logs. -- `GET /logs/:level`: Retrieve logs filtered by log level. -- `DELETE /logs`: Clear all backend logs. -- `DELETE /logs/:level`: Clear logs by log level. +## Documentation and Wiki -### Websocket -- `WS(S) /docker/stats`: Retrieve the current API configuration. - -## API - -The DockStat API exposes the following endpoints: - -| Endpoint | Method | Description | -| --- | --- | --- | -| `/docker/containers` | `GET` | Retrieve statistics for all containers across all configured Docker hosts. | -| `/docker/hosts/:id` | `GET` | Retrieve configuration and statistics for a specific Docker host. | -| `/docker-config/add-host` | `POST` | Add a new Docker host. | -| `/docker-config/update-host` | `POST` | Update an existing Docker host. | -| `/docker-config/hosts` | `GET` | Retrieve a list of all configured Docker hosts. | -| `/config/get` | `GET` | Retrieve the current API configuration. | -| `/config/update` | `POST` | Update the API configuration. | -| `/logs` | `GET` | Retrieve all backend logs. | -| `/logs/:level` | `GET` | Retrieve logs filtered by log level. | -| `/logs` | `DELETE` | Clear all backend logs. | -| `/logs/:level` | `DELETE` | Clear logs by log level. | - -## Contributing - -1. Fork the repository. -2. Create a new branch for your feature or bug fix. -3. Make your changes and commit them. -4. Push your branch to your forked repository. -5. Submit a pull request to the main repository. - -## License - -This project is licensed under the CC BY-NC 4.0 License. -![cc-by-nc-image](https://licensebuttons.net/l/by-nc/4.0/88x31.png) - -## Testing - -To run the tests, execute the following command: -(Currently no tests configured!) -``` -bun test -``` - -This will run the test suite and report the results. +Please see [DockStatAPI](https://dockstatapi.itsnik.de) diff --git a/bun.lock b/bun.lock index ba25a0f..5ab6675 100644 --- a/bun.lock +++ b/bun.lock @@ -7,20 +7,25 @@ "@elysiajs/server-timing": "^1.2.1", "@elysiajs/static": "^1.2.0", "@elysiajs/swagger": "^1.2.2", + "@elysiajs/trpc": "^1.1.0", + "@elysiajs/websocket": "^0.2.8", + "@trpc/server": "^10.45.2", "chalk": "^5.4.1", "docker-compose": "^1.1.1", "dockerode": "^4.0.4", "elysia": "latest", "split2": "^4.2.0", "winston": "^3.17.0", - "winston-transport": "^4.9.0", "yaml": "^2.7.0", }, "devDependencies": { "@types/dockerode": "^3.3.34", + "@types/node": "^22.13.10", "@types/split2": "^4.2.3", "bun-types": "latest", "cross-env": "^7.0.3", + "knip": "^5.46.0", + "typescript": "^5.8.2", "wrap-ansi": "^9.0.0", }, }, @@ -41,12 +46,22 @@ "@elysiajs/swagger": ["@elysiajs/swagger@1.2.2", "", { "dependencies": { "@scalar/themes": "^0.9.52", "@scalar/types": "^0.0.12", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.2.0" } }, "sha512-DG0PbX/wzQNQ6kIpFFPCvmkkWTIbNWDS7lVLv3Puy6ONklF14B4NnbDfpYjX1hdSYKeCqKBBOuenh6jKm8tbYA=="], + "@elysiajs/trpc": ["@elysiajs/trpc@1.1.0", "", { "peerDependencies": { "elysia": ">= 1.1.0" } }, "sha512-M8QWC+Wa5Z5MWY/+uMQuwZ+JoQkp4jOc1ra4SncFy1zSjFGin59LO1AT0pE+DRJaFV17gha9y7cB6Q7GnaJEAw=="], + + "@elysiajs/websocket": ["@elysiajs/websocket@0.2.8", "", { "dependencies": { "nanoid": "^4.0.0", "raikiri": "^0.0.0-beta.3" }, "peerDependencies": { "elysia": ">= 0.2.2" } }, "sha512-K9KLmYL1SYuAV353GvmK0V9DG5w7XTOGsa1H1dGB5BUTzvBaMvnwNeqnJQ3cjf9V1c0EjQds0Ty4LfUFvV45jw=="], + "@grpc/grpc-js": ["@grpc/grpc-js@1.12.6", "", { "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-JXUj6PI0oqqzTGvKtzOkxtpsyPRNsrmhh41TtIz/zEB6J+AUiZZ0dxWzcMwO9Ns5rmSPuMdghlTbUuqIM48d3Q=="], "@grpc/proto-loader": ["@grpc/proto-loader@0.7.13", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.2.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw=="], "@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="], + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@4.0.1", "", { "dependencies": { "@nodelib/fs.stat": "4.0.0", "run-parallel": "^1.2.0" } }, "sha512-vAkI715yhnmiPupY+dq+xenu5Tdf2TBQ66jLvBIcCddtz+5Q8LbMKaf9CIJJreez8fQ8fgaY+RaywQx8RJIWpw=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@3.0.1", "", { "dependencies": { "@nodelib/fs.scandir": "4.0.1", "fastq": "^1.15.0" } }, "sha512-nIh/M6Kh3ZtOmlY00DaUYB4xeeV6F3/ts1l29iwl3/cfyY/OuCfUx+v08zgx8TKPTifXRcjjqVQ4KB2zOYSbyw=="], + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], @@ -75,11 +90,15 @@ "@sinclair/typebox": ["@sinclair/typebox@0.34.27", "", {}, "sha512-C7mxE1VC3WC2McOufZXEU48IfRVI+BcKxk4NOyNn3+JMUNdJHEWGS5CqjuDX+ij2NCCz8/nse1mT7yn8Fv2GHg=="], + "@snyk/github-codeowners": ["@snyk/github-codeowners@1.1.0", "", { "dependencies": { "commander": "^4.1.1", "ignore": "^5.1.8", "p-map": "^4.0.0" }, "bin": { "github-codeowners": "dist/cli.js" } }, "sha512-lGFf08pbkEac0NYgVf4hdANpAgApRjNByLXB+WBip3qj1iendOIyAwP2GKkKbQMNVy2r1xxDf0ssfWscoiC+Vw=="], + + "@trpc/server": ["@trpc/server@10.45.2", "", {}, "sha512-wOrSThNNE4HUnuhJG6PfDRp4L2009KDVxsd+2VYH8ro6o/7/jwYZ8Uu5j+VaW+mOmc8EHerHzGcdbGNQSAUPgg=="], + "@types/docker-modem": ["@types/docker-modem@3.0.6", "", { "dependencies": { "@types/node": "*", "@types/ssh2": "*" } }, "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg=="], "@types/dockerode": ["@types/dockerode@3.3.34", "", { "dependencies": { "@types/docker-modem": "*", "@types/node": "*", "@types/ssh2": "*" } }, "sha512-mH9SuIb8NuTDsMus5epcbTzSbEo52fKLBMo0zapzYIAIyfDqoIFn7L3trekHLKC8qmxGV++pPUP4YqQ9n5v2Zg=="], - "@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], + "@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="], "@types/split2": ["@types/split2@4.2.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-59OXIlfUsi2k++H6CHgUQKEb2HKRokUA39HY1i1dS8/AIcqVjtAAFdf8u+HxTWK/4FUHMJQlKSZ4I6irCBJ1Zw=="], @@ -91,10 +110,14 @@ "@unhead/schema": ["@unhead/schema@1.11.19", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-7VhYHWK7xHgljdv+C01MepCSYZO2v6OhgsfKWPxRQBDDGfUKCUaChox0XMq3tFvXP6u4zSp6yzcDw2yxCfVMwg=="], - "ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + "aggregate-error": ["aggregate-error@3.1.0", "", { "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" } }, "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="], "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], @@ -105,6 +128,8 @@ "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], "buildcheck": ["buildcheck@0.0.6", "", {}, "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A=="], @@ -115,6 +140,8 @@ "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], + "clean-stack": ["clean-stack@2.2.0", "", {}, "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="], + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], "clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="], @@ -129,6 +156,8 @@ "colorspace": ["colorspace@1.1.4", "", { "dependencies": { "color": "^3.1.3", "text-hex": "1.0.x" } }, "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w=="], + "commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], "cpu-features": ["cpu-features@0.0.10", "", { "dependencies": { "buildcheck": "~0.0.6", "nan": "^2.19.0" } }, "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA=="], @@ -139,12 +168,16 @@ "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], + "docker-compose": ["docker-compose@1.1.1", "", { "dependencies": { "yaml": "^2.2.2" } }, "sha512-UkIUz0LtzuO17Ijm6SXMGtfZMs7IvbNwvuJBiBuN93PIhr/n9/sbJMqpvYFaCBGfwu1ZM4PPPDgQzeeke4lEoA=="], "docker-modem": ["docker-modem@5.0.6", "", { "dependencies": { "debug": "^4.1.1", "readable-stream": "^3.5.0", "split-ca": "^1.0.1", "ssh2": "^1.15.0" } }, "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ=="], "dockerode": ["dockerode@4.0.4", "", { "dependencies": { "@balena/dockerignore": "^1.0.2", "@grpc/grpc-js": "^1.11.1", "@grpc/proto-loader": "^0.7.13", "docker-modem": "^5.0.6", "protobufjs": "^7.3.2", "tar-fs": "~2.0.1", "uuid": "^10.0.0" } }, "sha512-6GYP/EdzEY50HaOxTVTJ2p+mB5xDHTMJhS+UoGrVyS6VC+iQRh7kZ4FRpUYq6nziby7hPqWhOrFFUFTMUZJJ5w=="], + "easy-table": ["easy-table@1.2.0", "", { "dependencies": { "ansi-regex": "^5.0.1" }, "optionalDependencies": { "wcwidth": "^1.0.1" } }, "sha512-OFzVOv03YpvtcWGe5AayU5G2hgybsg3iqA6drU8UaoZyB9jLGMTrz9+asnLp/E+6qPh88yEI1gvyZFZ41dmgww=="], + "elysia": ["elysia@1.2.21", "", { "dependencies": { "@sinclair/typebox": "^0.34.27", "cookie": "^1.0.2", "memoirist": "^0.3.0", "openapi-types": "^12.1.3" }, "peerDependencies": { "typescript": ">= 5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-E9b1JcB7fiQ2ptk24W8OnBrMYUoKzffIXob9uTVUKhqOKxaXAd9UyWBeyr7JCDa/VD/b/9S8aIey9/YJsK5sLg=="], "emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], @@ -153,10 +186,18 @@ "end-of-stream": ["end-of-stream@1.4.4", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q=="], + "enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="], + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], + "fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="], + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + "fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="], "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], @@ -165,20 +206,40 @@ "get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="], + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="], + + "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], + + "knip": ["knip@5.46.0", "", { "dependencies": { "@nodelib/fs.walk": "3.0.1", "@snyk/github-codeowners": "1.1.0", "easy-table": "1.2.0", "enhanced-resolve": "^5.18.0", "fast-glob": "^3.3.3", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "minimist": "^1.2.8", "picocolors": "^1.1.0", "picomatch": "^4.0.1", "pretty-ms": "^9.0.0", "smol-toml": "^1.3.1", "strip-json-comments": "5.0.1", "summary": "2.1.0", "zod": "^3.22.4", "zod-validation-error": "^3.0.3" }, "peerDependencies": { "@types/node": ">=18", "typescript": ">=5.0.4" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-WedHSK5xNBWYgm64Rt5B9b0CVXL2kRBcyCeet3NHgdv9en3QE4AWSDPEiX48NoPUBW3h//9S0VwLF5MG/MPi3g=="], + "kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="], "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], @@ -189,12 +250,20 @@ "memoirist": ["memoirist@0.3.0", "", {}, "sha512-wR+4chMgVPq+T6OOsk40u9Wlpw1Pjx66NMNiYxCQQ4EUJ7jDs3D9kTCeKdBOkvAiqXlHLVJlvYL01PvIJ1MPNg=="], + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "nan": ["nan@2.22.1", "", {}, "sha512-pfRR4ZcNTSm2ZFHaztuvbICf+hyiG6ecA06SfAxoPmuHjvMu0KUIae7Y8GyVkbBqeEIidsmXeYooWIX9+qjfRQ=="], + "nanoid": ["nanoid@4.0.2", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw=="], + "node-cache": ["node-cache@5.1.2", "", { "dependencies": { "clone": "2.x" } }, "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg=="], "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], @@ -203,18 +272,36 @@ "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], + "p-map": ["p-map@4.0.0", "", { "dependencies": { "aggregate-error": "^3.0.0" } }, "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ=="], + + "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], + + "pretty-ms": ["pretty-ms@9.2.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg=="], + "protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], "pump": ["pump@3.0.2", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw=="], + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "raikiri": ["raikiri@0.0.0-beta.8", "", {}, "sha512-cH/yfvkiGkN8IBB2MkRHikpPurTnd2sMkQ/xtGpXrp3O76P4ppcWPb+86mJaBDzKaclLnSX+9NnT79D7ifH4/w=="], + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], @@ -227,6 +314,8 @@ "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], + "smol-toml": ["smol-toml@1.3.1", "", {}, "sha512-tEYNll18pPKHroYSmLLrksq233j021G0giwW7P3D24jC54pQ5W5BXMsQ/Mvw1OJCmEYDgY+lrzT+3nNUtoNfXQ=="], + "split-ca": ["split-ca@1.0.1", "", {}, "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ=="], "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], @@ -241,12 +330,20 @@ "strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + "strip-json-comments": ["strip-json-comments@5.0.1", "", {}, "sha512-0fk9zBqO67Nq5M/m45qHCJxylV/DhBlIOVExqgOMiCCrzrhU6tCibRXNqE3jwJLftzE9SNuZtYbpzcO+i9FiKw=="], + + "summary": ["summary@2.1.0", "", {}, "sha512-nMIjMrd5Z2nuB2RZCKJfFMjgS3fygbeyGk9PxPPaJR1RIcyN9yn4A63Isovzm3ZtQuEkLBVgMdPup8UeLH7aQw=="], + + "tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="], + "tar-fs": ["tar-fs@2.0.1", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.0.0" } }, "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA=="], "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], "text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="], + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + "triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="], "tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="], @@ -259,6 +356,8 @@ "uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], + "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "winston": ["winston@3.17.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw=="], @@ -279,16 +378,42 @@ "zhead": ["zhead@2.2.4", "", {}, "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag=="], + "zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="], + + "zod-validation-error": ["zod-validation-error@3.4.0", "", { "peerDependencies": { "zod": "^3.18.0" } }, "sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ=="], + + "@nodelib/fs.scandir/@nodelib/fs.stat": ["@nodelib/fs.stat@4.0.0", "", {}, "sha512-ctr6bByzksKRCV0bavi8WoQevU6plSp2IkllIsEqaiKe2mwNNnaluhnRhcsgGZHrrHk57B3lf95MkLMO3STYcg=="], + "@scalar/themes/@scalar/types": ["@scalar/types@0.0.34", "", { "dependencies": { "@scalar/openapi-types": "0.1.8", "@unhead/schema": "^1.11.11" } }, "sha512-q01ctijmHArM5KOny2zU+sHfhpsgOAENrDENecK2TsQNn5FYLmFZouMKeW2M6F7KFLPZnFxUiL/rT88b6Rp/Kg=="], + "@types/docker-modem/@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], + + "@types/dockerode/@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], + + "@types/split2/@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], + "@types/ssh2/@types/node": ["@types/node@18.19.76", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-yvR7Q9LdPz2vGpmpJX5LolrgRdWvB67MJKDPSgIIzpFbaf9a1j/f5DnLp5VDyHGMR0QZHlTr1afsD87QCXFHKw=="], + "@types/ws/@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], + + "bun-types/@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], + "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "defaults/clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], + + "fast-glob/@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "protobufjs/@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], + + "strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.1.8", "", {}, "sha512-iufA5/6hPCmRIVD2eh7qGpoKvoA08Gw/qUb2JECifBtAwA93fo7+1k9uHK440f2LMJsbxIzA+nv7RS0BmfiO/g=="], @@ -297,18 +422,16 @@ "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "fast-glob/@nodelib/fs.walk/@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + "yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "cliui/wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "cliui/wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], } } diff --git a/package.json b/package.json index d194b46..55a9e43 100644 --- a/package.json +++ b/package.json @@ -17,30 +17,36 @@ "build": "bun build --target bun src/index.ts --outdir ./dist", "clean": "bun run clean:win || bun run clean:lin", "clean:win": "node -e \"process.exit(process.platform === 'win32' ? 0 : 1)\" && cmd /c del /Q dockstatapi.db* && echo 'success'", - "clean:lin": "node -e \"process.exit(process.platform !== 'win32' ? 0 : 1)\" && rm -f dockstatapi.db* && echo 'success'" + "clean:lin": "node -e \"process.exit(process.platform !== 'win32' ? 0 : 1)\" && rm -f dockstatapi.db* && echo 'success'", + "knip": "knip" }, "dependencies": { "@elysiajs/server-timing": "^1.2.1", "@elysiajs/static": "^1.2.0", "@elysiajs/swagger": "^1.2.2", + "@elysiajs/trpc": "^1.1.0", + "@elysiajs/websocket": "^0.2.8", + "@trpc/server": "^10.45.2", "chalk": "^5.4.1", "docker-compose": "^1.1.1", "dockerode": "^4.0.4", "elysia": "latest", "split2": "^4.2.0", "winston": "^3.17.0", - "winston-transport": "^4.9.0", "yaml": "^2.7.0" }, "devDependencies": { "@types/dockerode": "^3.3.34", + "@types/node": "^22.13.10", "@types/split2": "^4.2.3", "bun-types": "latest", "cross-env": "^7.0.3", + "knip": "^5.46.0", + "typescript": "^5.8.2", "wrap-ansi": "^9.0.0" }, "module": "src/index.js", "trustedDependencies": [ "protobufjs" ] -} \ No newline at end of file +} diff --git a/src/core/database/helper.ts b/src/core/database/helper.ts index ec9c1a7..753bc89 100644 --- a/src/core/database/helper.ts +++ b/src/core/database/helper.ts @@ -3,15 +3,15 @@ import { logger } from "../utils/logger"; export function executeDbOperation( label: string, operation: () => T, - validate?: () => void, + validate?: () => void ): T { const startTime = Date.now(); - logger.debug(`__task__ __db__ ${label} �3`); + logger.debug(`__task__ __db__ ${label} ⏳`); if (validate) { validate(); } const result = operation(); const duration = Date.now() - startTime; - logger.debug(`__task__ __db__ ${label} �4f (${duration}ms)`); + logger.debug(`__task__ __db__ ${label} ✔️ (${duration}ms)`); return result; } diff --git a/src/core/database/repository.ts b/src/core/database/repository.ts index a2c01fb..37f6ba5 100644 --- a/src/core/database/repository.ts +++ b/src/core/database/repository.ts @@ -1,7 +1,6 @@ import { executeDbOperation } from "./helper"; import Database from "bun:sqlite"; import { logger } from "~/core/utils/logger"; -import { relayController } from "~/core/docker/relay-controller"; import type { DockerHost, HostStats } from "~/typings/docker"; import type { stacks_config } from "~/typings/database"; @@ -66,7 +65,6 @@ export const dbFunctions = { ); CREATE TABLE IF NOT EXISTS config ( - polling_rate NUMBER, keep_data_for NUMBER, fetching_interval NUMBER ); @@ -76,7 +74,6 @@ export const dbFunctions = { /* * Default values: - * - Websocket polling interval 5 seconds * - Data retention value for the database (logs and container stats) 7 days * - Data fetcher for the Database: 5 minutes */ @@ -87,8 +84,8 @@ export const dbFunctions = { logger.debug("Initializing default config"); const stmt = db.prepare( ` - INSERT INTO config (polling_rate, keep_data_for, fetching_interval) VALUES (5, 7, 5) - `, + INSERT INTO config (keep_data_for, fetching_interval) VALUES (7, 5) + ` ); stmt.run(); } @@ -101,7 +98,7 @@ export const dbFunctions = { const stmt = db.prepare( ` INSERT INTO docker_hosts (name, url, secure) VALUES (?, ?, ?) - `, + ` ); stmt.run("Localhost", "localhost:2375", false); } @@ -129,7 +126,7 @@ export const dbFunctions = { logger.error("Invalid parameter types for addDockerHost"); throw new TypeError("Invalid parameter types for addDockerHost"); } - }, + } ); }, @@ -145,7 +142,7 @@ export const dbFunctions = { const data = stmt.all(); return data as DockerHost[]; }, - () => {}, + () => {} ); }, @@ -153,7 +150,7 @@ export const dbFunctions = { level: string, message: string, file_name: string, - line: number, + line: number ) => { if ( typeof level !== "string" || @@ -184,7 +181,7 @@ export const dbFunctions = { const data = stmt.all(); return data; }, - () => {}, + () => {} ); }, @@ -206,7 +203,7 @@ export const dbFunctions = { logger.error("Level parameter must be a string"); throw new TypeError("Level parameter must be a string"); } - }, + } ); }, @@ -231,7 +228,7 @@ export const dbFunctions = { logger.error("Invalid parameter types for updateDockerHost"); throw new TypeError("Invalid parameter types for updateDockerHost"); } - }, + } ); }, @@ -251,7 +248,7 @@ export const dbFunctions = { logger.error("Invalid parameter type for deleteDockerHost"); throw new TypeError("Name parameter must be a string"); } - }, + } ); }, @@ -265,7 +262,7 @@ export const dbFunctions = { const data = stmt.run(); return data; }, - () => {}, + () => {} ); }, @@ -285,37 +282,31 @@ export const dbFunctions = { logger.error("Invalid parameter type for clearLogsByLevel"); throw new TypeError("Level parameter must be a string"); } - }, + } ); }, - updateConfig( - polling_rate: number, - fetching_interval: number, - keep_data_for: number, - ) { + updateConfig(fetching_interval: number, keep_data_for: number) { return executeDbOperation( "Update Config", () => { const stmt = db.prepare(` UPDATE config - SET polling_rate = ?, - fetching_interval = ?, + SET fetching_interval = ?, keep_data_for = ? `); - const data = stmt.run(polling_rate, fetching_interval, keep_data_for); + const data = stmt.run(fetching_interval, keep_data_for); return data; }, () => { if ( - typeof polling_rate !== "number" || typeof fetching_interval !== "number" || typeof keep_data_for !== "number" ) { logger.error("Invalid parameter types for updateConfig"); throw new TypeError("Invalid parameter types for updateConfig"); } - }, + } ); }, @@ -324,13 +315,13 @@ export const dbFunctions = { "Get Config", () => { const stmt = db.prepare(` - SELECT polling_rate, keep_data_for, fetching_interval + SELECT keep_data_for, fetching_interval FROM config `); const data = stmt.all(); return data; }, - () => {}, + () => {} ); }, @@ -355,7 +346,7 @@ export const dbFunctions = { logger.error("Invalid parameter type for deleteOldData"); throw new TypeError("Days parameter must be a number"); } - }, + } ); }, @@ -367,7 +358,7 @@ export const dbFunctions = { status: string, state: string, cpu_usage: number, - memory_usage: number, + memory_usage: number ) { return executeDbOperation( "Add Container Stats", @@ -384,7 +375,7 @@ export const dbFunctions = { status, state, cpu_usage, - memory_usage, + memory_usage ); return data; }, @@ -402,7 +393,7 @@ export const dbFunctions = { logger.error("Invalid parameter types for addContainerStats"); throw new TypeError("Invalid parameter types for addContainerStats"); } - }, + } ); }, @@ -455,11 +446,11 @@ export const dbFunctions = { stats.containersRunning, stats.containersStopped, stats.containersPaused, - stats.images, + stats.images ); return data; }, - () => {}, + () => {} ); }, @@ -488,12 +479,11 @@ export const dbFunctions = { stack_config.container_count, stack_config.stack_prefix, stack_config.automatic_reboot_on_error, - stack_config.image_updates, + stack_config.image_updates ); - relayController.stackAdded(); return data; }, - () => {}, + () => {} ); }, @@ -509,7 +499,7 @@ export const dbFunctions = { const data = stmt.all(); return data; }, - () => {}, + () => {} ); }, @@ -522,10 +512,9 @@ export const dbFunctions = { WHERE name = ?; `); const data = stmt.run(name); - relayController.stackDeleted(); return data; }, - () => {}, + () => {} ); }, @@ -553,12 +542,11 @@ export const dbFunctions = { stack_config.stack_prefix, stack_config.automatic_reboot_on_error, stack_config.image_updates, - stack_config.name, + stack_config.name ); - relayController.stackUpdated(); return data; }, - () => {}, + () => {} ); }, }; diff --git a/src/core/docker/client.ts b/src/core/docker/client.ts index b97ef13..010a2bd 100644 --- a/src/core/docker/client.ts +++ b/src/core/docker/client.ts @@ -2,14 +2,6 @@ import type { DockerHost } from "~/typings/docker"; import Docker from "dockerode"; import { logger } from "~/core/utils/logger"; -async function fileExists(path: string): Promise { - try { - return await Bun.file(path).exists(); - } catch (error) { - return false; - } -} - export const getDockerClient = (host: DockerHost): Docker => { try { const inputUrl = host.url.includes("://") @@ -20,8 +12,8 @@ export const getDockerClient = (host: DockerHost): Docker => { let port = parsedUrl.port ? parseInt(parsedUrl.port) : host.secure - ? 2376 - : 2375; + ? 2376 + : 2375; if (isNaN(port) || port < 1 || port > 65535) { throw new Error("Invalid port number in Docker host URL"); @@ -39,31 +31,3 @@ export const getDockerClient = (host: DockerHost): Docker => { throw new Error("Invalid Docker host configuration"); } }; - -export const stackClient = async (): Promise => { - const socketPath = "/var/run/docker.sock"; - try { - if (!(await fileExists(socketPath))) { - throw new Error("Docker socket not found at " + socketPath); - } - - const docker = new Docker({ - socketPath, - }); - - const pingTimeout = 2000; - await Promise.race([ - docker.ping(), - new Promise((_, reject) => - setTimeout(() => reject(new Error("Ping timed out")), pingTimeout), - ), - ]); - - return docker; - } catch (error) { - logger.error( - `Could not create Docker client for "${socketPath}" - ${error}`, - ); - throw new Error("Failed to create Docker client for local Docker socket"); - } -}; diff --git a/src/core/docker/relay-controller.ts b/src/core/docker/relay-controller.ts deleted file mode 100644 index f99314d..0000000 --- a/src/core/docker/relay-controller.ts +++ /dev/null @@ -1,7 +0,0 @@ -// Import any function here, when any of the specifies functions is detected, it will run said function - -export const relayController = { - stackAdded() {}, - stackDeleted() {}, - stackUpdated() {}, -}; diff --git a/src/core/plugins/plugin-actions.ts b/src/core/plugins/plugin-actions.ts index 0b2f935..f914681 100644 --- a/src/core/plugins/plugin-actions.ts +++ b/src/core/plugins/plugin-actions.ts @@ -4,7 +4,4 @@ export const pluginAction = { containerStart(containerInfo: any) { pluginManager.handleContainerStart(containerInfo); }, - metricsReceived(metrics: any) { - pluginManager.handleMetrics(metrics); - }, }; diff --git a/src/core/plugins/plugin-manager.ts b/src/core/plugins/plugin-manager.ts index 614604a..a81aa0e 100644 --- a/src/core/plugins/plugin-manager.ts +++ b/src/core/plugins/plugin-manager.ts @@ -12,7 +12,7 @@ export class PluginManager extends EventEmitter { logger.debug(`Registered plugin: ${plugin.name}`); } catch (error) { logger.error( - `Registering plugin ${plugin.name} failed: ${error as string}`, + `Registering plugin ${plugin.name} failed: ${error as string}` ); } } @@ -28,6 +28,12 @@ export class PluginManager extends EventEmitter { }); } + handleContainerStart(containerInfo: ContainerInfo) { + this.plugins.forEach((plugin) => { + plugin.onContainerStart?.(containerInfo); + }); + } + handleContainerExit(containerInfo: ContainerInfo) { this.plugins.forEach((plugin) => { plugin.onContainerExit?.(containerInfo); diff --git a/src/core/trpc/README.md b/src/core/trpc/README.md new file mode 100644 index 0000000..32bdb3f --- /dev/null +++ b/src/core/trpc/README.md @@ -0,0 +1 @@ +Please see: [DockStatAPI tRPC Routes Reference](https://outline.itsnik.de/s/dockstat/doc/trpc-2hzqJ7BvA0) diff --git a/src/core/trpc/index.ts b/src/core/trpc/index.ts new file mode 100644 index 0000000..7a13655 --- /dev/null +++ b/src/core/trpc/index.ts @@ -0,0 +1,4 @@ +import { trpc } from "@elysiajs/trpc"; +import { appRouter } from "./router"; + +export default trpc(appRouter, { endpoint: "/trpc" }); diff --git a/src/core/trpc/procedures/api-config.procedure.ts b/src/core/trpc/procedures/api-config.procedure.ts new file mode 100644 index 0000000..bf6cd40 --- /dev/null +++ b/src/core/trpc/procedures/api-config.procedure.ts @@ -0,0 +1,79 @@ +import { dbFunctions } from "~/core/database/repository"; +import { logger } from "~/core/utils/logger"; +import { + version, + authorEmail, + authorName, + authorWebsite, + contributors, + dependencies, + description, + devDependencies, + license, +} from "~/core/utils/package-json"; +import { TRPCError } from "@trpc/server"; +import { z } from "zod"; +import { router, publicProcedure } from "../trpc"; +import { config } from "~/typings/database"; + +const configInputSchema = z.object({ + fetching_interval: z.number(), + keep_data_for: z.number(), +}); + +export const configProcedure = router({ + get: publicProcedure.query(() => { + try { + const data = dbFunctions.getConfig() as config[]; + const distinct = data[0]; + logger.debug("tRPC: Fetched backend config"); + return distinct; + } catch (error) { + logger.error("tRPC config get error", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Error getting the DockStatAPI config", + cause: error, + }); + } + }), + + update: publicProcedure.input(configInputSchema).mutation(({ input }) => { + try { + const { fetching_interval, keep_data_for } = input; + dbFunctions.updateConfig(fetching_interval, keep_data_for); + return { success: true, message: "Updated DockStatAPI config" }; + } catch (error) { + logger.error("tRPC config update error", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Error updating the DockStatAPI config", + cause: error, + }); + } + }), + + package: publicProcedure.query(() => { + try { + logger.debug("tRPC: Fetching package.json"); + return { + version, + description, + license, + authorName, + authorEmail, + authorWebsite, + contributors, + dependencies, + devDependencies, + }; + } catch (error) { + logger.error("tRPC package info error", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Error while reading package.json", + cause: error, + }); + } + }), +}); diff --git a/src/core/trpc/procedures/docker-manager.procedure.ts b/src/core/trpc/procedures/docker-manager.procedure.ts new file mode 100644 index 0000000..958b31b --- /dev/null +++ b/src/core/trpc/procedures/docker-manager.procedure.ts @@ -0,0 +1,65 @@ +import { dbFunctions } from "~/core/database/repository"; +import { logger } from "~/core/utils/logger"; +import { TRPCError } from "@trpc/server"; +import { z } from "zod"; +import { router, publicProcedure } from "../trpc"; + +const addHostInput = z.object({ + name: z.string(), + url: z.string(), + secure: z.boolean(), +}); + +const updateHostInput = z.object({ + name: z.string(), + url: z.string(), + secure: z.boolean(), +}); + +export const dockerManagerProcedure = router({ + addHost: publicProcedure.input(addHostInput).mutation(({ input }) => { + try { + const { name, url, secure } = input; + dbFunctions.addDockerHost(name, url, secure); + logger.debug(`Added docker host (${name})`); + return { success: true, message: `Added docker host (${name})` }; + } catch (error) { + logger.error("Error adding docker host", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Error adding docker host", + cause: error, + }); + } + }), + + updateHost: publicProcedure.input(updateHostInput).mutation(({ input }) => { + try { + const { name, url, secure } = input; + dbFunctions.updateDockerHost(name, url, secure); + return { success: true, message: `Updated docker host (${name})` }; + } catch (error) { + logger.error("Error updating docker host", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to update host", + cause: error, + }); + } + }), + + getHosts: publicProcedure.query(() => { + try { + const dockerHosts = dbFunctions.getDockerHosts(); + logger.debug("Retrieved docker hosts via tRPC"); + return dockerHosts; + } catch (error) { + logger.error("Error retrieving docker hosts", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to retrieve hosts", + cause: error, + }); + } + }), +}); diff --git a/src/core/trpc/procedures/docker-stats.procedure.ts b/src/core/trpc/procedures/docker-stats.procedure.ts new file mode 100644 index 0000000..017e880 --- /dev/null +++ b/src/core/trpc/procedures/docker-stats.procedure.ts @@ -0,0 +1,147 @@ +import Docker from "dockerode"; +import { dbFunctions } from "~/core/database/repository"; +import { getDockerClient } from "~/core/docker/client"; +import { + calculateCpuPercent, + calculateMemoryUsage, +} from "~/core/utils/calculations"; +import { logger } from "~/core/utils/logger"; +import { TRPCError } from "@trpc/server"; +import { z } from "zod"; +import { router, publicProcedure } from "../trpc"; +import type { ContainerInfo, DockerHost, HostStats } from "~/typings/docker"; +import type { DockerInfo } from "~/typings/dockerode"; + +export const dockerStatsProcedure = router({ + getContainers: publicProcedure.query(async () => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + const containers: ContainerInfo[] = []; + + await Promise.all( + hosts.map(async (host) => { + try { + const docker = getDockerClient(host); + try { + await docker.ping(); + } catch (pingError) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Docker host connection failed", + cause: pingError, + }); + } + + const hostContainers = await docker.listContainers({ all: true }); + + await Promise.all( + hostContainers.map(async (containerInfo) => { + try { + const container = docker.getContainer(containerInfo.Id); + const stats = await new Promise( + (resolve, reject) => { + container.stats({ stream: false }, (error, stats) => { + if (error) { + reject( + new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Error fetching container stats", + cause: error, + }) + ); + } + if (!stats) { + reject( + new TRPCError({ + code: "NOT_FOUND", + message: "No stats available", + }) + ); + } + resolve(stats as Docker.ContainerStats); + }); + } + ); + + containers.push({ + id: containerInfo.Id, + hostId: host.name, + name: containerInfo.Names[0].replace(/^\//, ""), + image: containerInfo.Image, + status: containerInfo.Status, + state: containerInfo.State, + cpuUsage: calculateCpuPercent(stats), + memoryUsage: calculateMemoryUsage(stats), + }); + } catch (containerError) { + logger.error( + "Error fetching container stats", + containerError + ); + } + }) + ); + logger.debug(`Fetched stats for ${host.name}`); + } catch (hostError) { + logger.error("Error fetching containers for host", hostError); + } + }) + ); + + logger.debug("Fetched all containers across all hosts"); + return { containers }; + } catch (error) { + logger.error("Error fetching containers", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to retrieve containers", + cause: error, + }); + } + }), + + getHostStats: publicProcedure + .input(z.object({ id: z.string() })) + .query(async ({ input }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + const host = hosts.find((h) => h.name === input.id); + + if (!host) { + throw new TRPCError({ + code: "NOT_FOUND", + message: `Host (${input.id}) not found`, + }); + } + + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); + + const config: HostStats = { + hostId: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; + + logger.debug(`Fetched config for ${host.name}`); + return config; + } catch (error) { + logger.error("Error fetching host stats", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to retrieve host config", + cause: error, + }); + } + }), +}); diff --git a/src/core/trpc/procedures/logs.procedure.ts b/src/core/trpc/procedures/logs.procedure.ts new file mode 100644 index 0000000..520a2cb --- /dev/null +++ b/src/core/trpc/procedures/logs.procedure.ts @@ -0,0 +1,73 @@ +import { dbFunctions } from "~/core/database/repository"; +import { logger } from "~/core/utils/logger"; +import { TRPCError } from "@trpc/server"; +import { z } from "zod"; +import { router, publicProcedure } from "../trpc"; + +const logLevelSchema = z.enum(["debug", "info", "warn", "error"]); + +export const logsProcedure = router({ + getAll: publicProcedure.query(() => { + try { + const logs = dbFunctions.getAllLogs(); + logger.debug("Retrieved all logs via tRPC"); + return logs; + } catch (error) { + logger.error("Failed to retrieve logs", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to retrieve logs", + cause: error, + }); + } + }), + + getByLevel: publicProcedure + .input(z.object({ level: logLevelSchema })) + .query(({ input }) => { + try { + const logs = dbFunctions.getLogsByLevel(input.level); + logger.debug(`Retrieved logs (level: ${input.level}) via tRPC`); + return logs; + } catch (error) { + logger.error("Failed to retrieve logs by level", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to retrieve logs by level", + cause: error, + }); + } + }), + + clearAll: publicProcedure.mutation(() => { + try { + dbFunctions.clearAllLogs(); + logger.debug("Cleared all logs via tRPC"); + return { success: true }; + } catch (error) { + logger.error("Failed to clear all logs", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Could not delete all logs", + cause: error, + }); + } + }), + + clearByLevel: publicProcedure + .input(z.object({ level: logLevelSchema })) + .mutation(({ input }) => { + try { + dbFunctions.clearLogsByLevel(input.level); + logger.debug(`Cleared logs (level: ${input.level}) via tRPC`); + return { success: true }; + } catch (error) { + logger.error("Failed to clear logs by level", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Could not clear logs by level", + cause: error, + }); + } + }), +}); diff --git a/src/core/trpc/procedures/stacks.procedure.ts b/src/core/trpc/procedures/stacks.procedure.ts new file mode 100644 index 0000000..6aad4e3 --- /dev/null +++ b/src/core/trpc/procedures/stacks.procedure.ts @@ -0,0 +1,199 @@ +import { dbFunctions } from "~/core/database/repository"; +import { logger } from "~/core/utils/logger"; +import { TRPCError } from "@trpc/server"; +import { z } from "zod"; +import { router, publicProcedure } from "../trpc"; +import { + deployStack, + stopStack, + pullStackImages, + restartStack, + getStackStatus, + startStack, +} from "~/core/stacks/controller"; + +const deployStackInput = z.object({ + compose_spec: z.any(), + name: z.string(), + version: z.number(), + automatic_reboot_on_error: z.boolean(), + isCustom: z.boolean().optional(), + image_updates: z.boolean().optional(), + source: z.string(), + stack_prefix: z.string().optional(), +}); + +const stackOperationInput = z.object({ + stack: z.any(), +}); + +const stackStatusInput = z.object({ + stack_name: z.any(), +}); + +export const stacksProcedure = router({ + deploy: publicProcedure + .input(deployStackInput) + .mutation(async ({ input }) => { + try { + const missingParams = []; + if (!input.compose_spec) missingParams.push("compose_spec"); + if (!input.automatic_reboot_on_error) + missingParams.push("automatic_reboot_on_error"); + if (!input.source) missingParams.push("source"); + if (!input.name) missingParams.push("name"); + + if (missingParams.length > 0) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: `Missing values: ${missingParams.join(", ")}`, + }); + } + + await deployStack( + input.compose_spec, + input.name, + input.version, + input.source, + input.automatic_reboot_on_error, + input.isCustom || false, + input.image_updates || false, + input.stack_prefix + ); + + logger.info(`Deployed Stack (${input.name}) via tRPC`); + return { + success: true, + message: `Stack ${input.name} deployed successfully`, + }; + } catch (error) { + logger.error("Error deploying stack", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + error instanceof Error ? error.message : "Error deploying stack", + cause: error, + }); + } + }), + + start: publicProcedure + .input(stackOperationInput) + .mutation(async ({ input }) => { + try { + await startStack(input.stack); + logger.info(`Started Stack (${input.stack}) via tRPC`); + return { + success: true, + message: `Stack ${input.stack} started successfully`, + }; + } catch (error) { + logger.error("Error starting stack", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + error instanceof Error ? error.message : "Error starting stack", + cause: error, + }); + } + }), + + stop: publicProcedure + .input(stackOperationInput) + .mutation(async ({ input }) => { + try { + await stopStack(input.stack); + logger.info(`Stopped Stack (${input.stack}) via tRPC`); + return { + success: true, + message: `Stack ${input.stack} stopped successfully`, + }; + } catch (error) { + logger.error("Error stopping stack", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + error instanceof Error ? error.message : "Error stopping stack", + cause: error, + }); + } + }), + + restart: publicProcedure + .input(stackOperationInput) + .mutation(async ({ input }) => { + try { + await restartStack(input.stack); + logger.info(`Restarted Stack (${input.stack}) via tRPC`); + return { + success: true, + message: `Stack ${input.stack} restarted successfully`, + }; + } catch (error) { + logger.error("Error restarting stack", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + error instanceof Error ? error.message : "Error restarting stack", + cause: error, + }); + } + }), + + pullImages: publicProcedure + .input(stackOperationInput) + .mutation(async ({ input }) => { + try { + await pullStackImages(input.stack); + logger.info(`Pulled Stack images (${input.stack}) via tRPC`); + return { + success: true, + message: `Images for stack ${input.stack} pulled successfully`, + }; + } catch (error) { + logger.error("Error pulling images", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + error instanceof Error ? error.message : "Error pulling images", + cause: error, + }); + } + }), + + getStatus: publicProcedure + .input(stackStatusInput) + .query(async ({ input }) => { + try { + const status = await getStackStatus(input.stack_name); + logger.info(`Fetched Stack status (${input.stack_name}) via tRPC`); + return { status }; + } catch (error) { + logger.error("Error getting stack status", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + error instanceof Error + ? error.message + : "Error getting stack status", + cause: error, + }); + } + }), + + getAll: publicProcedure.query(() => { + try { + const stacks = dbFunctions.getStacks(); + logger.info("Fetched Stacks via tRPC"); + return stacks; + } catch (error) { + logger.error("Error getting stacks", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + error instanceof Error ? error.message : "Error getting stacks", + cause: error, + }); + } + }), +}); diff --git a/src/core/trpc/router.ts b/src/core/trpc/router.ts new file mode 100644 index 0000000..acdd78d --- /dev/null +++ b/src/core/trpc/router.ts @@ -0,0 +1,21 @@ +import { router, t } from "./trpc"; +import { configProcedure } from "./procedures/api-config.procedure"; +import { dockerManagerProcedure } from "./procedures/docker-manager.procedure"; +import { dockerStatsProcedure } from "./procedures/docker-stats.procedure"; +import { logsProcedure } from "./procedures/logs.procedure"; +import { stacksProcedure } from "./procedures/stacks.procedure"; + +export const appRouter = router({ + config: configProcedure, + docker: router({ + manager: dockerManagerProcedure, + stats: dockerStatsProcedure, + }), + logs: logsProcedure, + stacks: stacksProcedure, + health: router({ + check: t.procedure.query(() => ({ status: "healthy" })), + }), +}); + +export type AppRouter = typeof appRouter; diff --git a/src/core/trpc/trpc.ts b/src/core/trpc/trpc.ts new file mode 100644 index 0000000..554f58d --- /dev/null +++ b/src/core/trpc/trpc.ts @@ -0,0 +1,5 @@ +import { initTRPC } from "@trpc/server"; + +export const t = initTRPC.create(); +export const router = t.router; +export const publicProcedure = t.procedure; diff --git a/src/index.ts b/src/index.ts index 975ebf9..482f924 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,7 @@ import { apiConfigRoutes } from "~/routes/api-config"; import { setSchedules } from "~/core/docker/scheduler"; import { serverTiming } from "@elysiajs/server-timing"; import staticPlugin from "@elysiajs/static"; +import trpcRouter from "~/core/trpc"; console.log(""); dbFunctions.init(); @@ -46,9 +47,10 @@ const DockStatAPI = new Elysia() }, ], }, - }), + }) ) .use(serverTiming()) + .use(trpcRouter) .use(dockerRoutes) .use(dockerStatsRoutes) .use(backendLogs) @@ -72,7 +74,10 @@ async function startServer() { console.log("----- [ ############## ]"); logger.info(`DockStatAPI is running at http://${hostname}:${port}`); logger.info( - `Swagger API Documentation available at http://${hostname}:${port}/swagger`, + `Swagger API Documentation available at http://${hostname}:${port}/swagger` + ); + logger.info( + `tRPC Endpoint available at: http://${hostname}:${port}/trpc` ); }); } catch (error) { diff --git a/src/plugins/example.plugin.ts b/src/plugins/example.plugin.ts index a9ed6ac..bd71bec 100644 --- a/src/plugins/example.plugin.ts +++ b/src/plugins/example.plugin.ts @@ -1,7 +1,6 @@ import type { Plugin } from "~/typings/plugin"; import type { ContainerInfo } from "~/typings/docker"; import type { HostStats } from "~/typings/docker"; -import { logger } from "~/core/utils/logger"; const ExamplePlugin: Plugin = { name: "Example Plugin", diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index 1cdda5e..bc08132 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -30,42 +30,37 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) return responseHandler.error( set, "Error getting the DockStatAPI config", - error as string, + error as string ); } }, { tags: ["Management"], - }, + } ) .post( "/update", async ({ set, body }) => { try { - const { polling_rate, fetching_interval, keep_data_for } = body; + const { fetching_interval, keep_data_for } = body; set.headers["Content-Type"] = "application/json"; - dbFunctions.updateConfig( - polling_rate, - fetching_interval, - keep_data_for, - ); + dbFunctions.updateConfig(fetching_interval, keep_data_for); return responseHandler.ok(set, "Updated DockStatAPI config"); } catch (error) { return responseHandler.error( set, "Error updating the DockStatAPI config", - error as string, + error as string ); } }, { body: t.Object({ - polling_rate: t.Number(), fetching_interval: t.Number(), keep_data_for: t.Number(), }), tags: ["Management"], - }, + } ) .get( "/package", @@ -87,11 +82,11 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) return responseHandler.error( set, error as string, - "Error while reading package.json", + "Error while reading package.json" ); } }, { tags: ["Management"], - }, + } ); diff --git a/src/routes/stacks.ts b/src/routes/stacks.ts index 3fcf911..f4be7b5 100644 --- a/src/routes/stacks.ts +++ b/src/routes/stacks.ts @@ -1,4 +1,4 @@ -import { Elysia, error, t } from "elysia"; +import { Elysia, t } from "elysia"; import { responseHandler } from "~/core/utils/respone-handler"; import { deployStack, @@ -47,18 +47,18 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) body.automatic_reboot_on_error, isCustom, image_updates, - body.stack_prefix, + body.stack_prefix ); logger.info(`Deployed Stack (${body.name})`); return responseHandler.ok( set, - `Stack ${body.name} deployed successfully`, + `Stack ${body.name} deployed successfully` ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error deploying stack", + "Error deploying stack" ); } }, @@ -74,7 +74,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) source: t.String(), stack_prefix: t.Optional(t.String()), }), - }, + } ) .post( "/start", @@ -87,13 +87,13 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) logger.info(`Started Stack (${body.stack})`); return responseHandler.ok( set, - `Stack ${body.stack} started successfully`, + `Stack ${body.stack} started successfully` ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error starting stack", + "Error starting stack" ); } }, @@ -102,7 +102,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) body: t.Object({ stack: t.Any(), }), - }, + } ) .post( "/stop", @@ -115,13 +115,13 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) logger.info(`Stopped Stack (${body.stack})`); return responseHandler.ok( set, - `Stack ${body.stack} stopped successfully`, + `Stack ${body.stack} stopped successfully` ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error stopping stack", + "Error stopping stack" ); } }, @@ -130,7 +130,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) body: t.Object({ stack: t.Any(), }), - }, + } ) .post( "/restart", @@ -143,13 +143,13 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) logger.info(`Restarted Stack (${body.stack})`); return responseHandler.ok( set, - `Stack ${body.stack} restarted successfully`, + `Stack ${body.stack} restarted successfully` ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error restarting stack", + "Error restarting stack" ); } }, @@ -158,7 +158,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) body: t.Object({ stack: t.Any(), }), - }, + } ) .post( "/pull-images", @@ -171,13 +171,13 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) logger.info(`Pulled Stack images (${body.stack})`); return responseHandler.ok( set, - `Images for stack ${body.stack} pulled successfully`, + `Images for stack ${body.stack} pulled successfully` ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error pulling images", + "Error pulling images" ); } }, @@ -186,7 +186,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) body: t.Object({ stack: t.Any(), }), - }, + } ) .get( "/status", @@ -199,7 +199,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) const status = await getStackStatus(query.stack_name); const res = responseHandler.ok( set, - `Stack ${query.stack_name} status retrieved successfully`, + `Stack ${query.stack_name} status retrieved successfully` ); logger.info("Fetched Stack status"); return { ...res, status: status }; @@ -207,7 +207,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) return responseHandler.error( set, error.message || error, - "Error getting stack status", + "Error getting stack status" ); } }, @@ -216,7 +216,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) query: t.Object({ stack_name: t.Any(), }), - }, + } ) .get( "/", @@ -229,11 +229,11 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) return responseHandler.error( set, error.message || error, - "Error getting stacks", + "Error getting stacks" ); } }, { detail: { tags: ["Stacks"] }, - }, + } ); diff --git a/src/typings/database.ts b/src/typings/database.ts index 3d95b35..c5200e6 100644 --- a/src/typings/database.ts +++ b/src/typings/database.ts @@ -7,10 +7,10 @@ interface backend_log_entries { } interface config { - polling_rate: number; keep_data_for: number; fetching_interval: number; } + interface stacks_config { name: string; version: number; From 4f195b4983557da57efd0447688f86f3cf1bf0e0 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 13 Mar 2025 23:03:02 +0100 Subject: [PATCH 167/324] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 37881b7..87b7750 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Docker monitoring API with real-time statistics, stack management, and plugin su - Plugin system for custom logic/notifications - Historical stats storage (SQLite) - Swagger API documentation -- Web dashboard (WIP) +- Web dashboard ([DockStat](https://github.com/its4nik/DockStat)) ## Tech Stack From a37465096494653b353646c6f9fd0c775d738ad8 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 13 Mar 2025 23:12:11 +0100 Subject: [PATCH 168/324] Feat: Dependency graph --- .github/scripts/generate-mermaid.js | 25 ++++++++++++++++++ .github/worrkflows/dependency-graph.yml | 35 +++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 .github/scripts/generate-mermaid.js create mode 100644 .github/worrkflows/dependency-graph.yml diff --git a/.github/scripts/generate-mermaid.js b/.github/scripts/generate-mermaid.js new file mode 100644 index 0000000..d030db1 --- /dev/null +++ b/.github/scripts/generate-mermaid.js @@ -0,0 +1,25 @@ +const fs = require('fs'); +const path = require('path'); + +const dependencies = JSON.parse(fs.readFileSync('dependencies.json', 'utf-8')); + +const edges = []; +Object.entries(dependencies).forEach(([file, deps]) => { + deps.forEach(dep => { + edges.push(` "${file}" --> "${dep}"`); + }); +}); + +const mermaidContent = `graph TD +${edges.join('\n')}`; + +const markdownDoc = `# DockStatAPI Dependency Graph + +\`\`\`mermaid +${mermaidContent} +\`\`\` +`; + +fs.writeFileSync(path.join("./", 'dependencies.md'), markdownDoc); + +console.log('Successfully generated dependency graph'); \ No newline at end of file diff --git a/.github/worrkflows/dependency-graph.yml b/.github/worrkflows/dependency-graph.yml new file mode 100644 index 0000000..630c24b --- /dev/null +++ b/.github/worrkflows/dependency-graph.yml @@ -0,0 +1,35 @@ +name: Generate Dependency Graph + +on: + push: + +jobs: + generate: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install madge + run: npm install -g madge + + - name: Generate Dependency Data + run: madge --json src/index.ts > dependencies.json + + - name: Generate Mermaid Markdown + run: node .github/scripts/generate-mermaid.js + + - name: Commit and Push Changes + uses: EndBug/add-and-commit@v9 + with: + add: 'dependencies.md' + message: 'CI/CD: Update dependency graph' + committer_name: 'GitHub Action' + committer_email: 'action@github.com' \ No newline at end of file From 31b5e3039bcc1268248ae9beaa709baf049c0176 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 13 Mar 2025 23:14:07 +0100 Subject: [PATCH 169/324] Fix: Typo --- .github/{worrkflows => workflows}/dependency-graph.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/{worrkflows => workflows}/dependency-graph.yml (100%) diff --git a/.github/worrkflows/dependency-graph.yml b/.github/workflows/dependency-graph.yml similarity index 100% rename from .github/worrkflows/dependency-graph.yml rename to .github/workflows/dependency-graph.yml From 29af2cf23742fb94e2d250b833a06b5ac5be7a55 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 13 Mar 2025 23:15:32 +0100 Subject: [PATCH 170/324] Fix: Permission --- .github/workflows/dependency-graph.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dependency-graph.yml b/.github/workflows/dependency-graph.yml index 630c24b..86d8e36 100644 --- a/.github/workflows/dependency-graph.yml +++ b/.github/workflows/dependency-graph.yml @@ -3,6 +3,8 @@ name: Generate Dependency Graph on: push: +permissions: write-all + jobs: generate: runs-on: ubuntu-latest @@ -29,7 +31,7 @@ jobs: - name: Commit and Push Changes uses: EndBug/add-and-commit@v9 with: - add: 'dependencies.md' - message: 'CI/CD: Update dependency graph' - committer_name: 'GitHub Action' - committer_email: 'action@github.com' \ No newline at end of file + add: "dependencies.md" + message: "CI/CD: Update dependency graph" + committer_name: "GitHub Action" + committer_email: "action@github.com" From 7b8c2c147bd057bdd426f996e77abc1257837e33 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Thu, 13 Mar 2025 22:15:55 +0000 Subject: [PATCH 171/324] CI/CD: Update dependency graph --- dependencies.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 dependencies.md diff --git a/dependencies.md b/dependencies.md new file mode 100644 index 0000000..fbb7268 --- /dev/null +++ b/dependencies.md @@ -0,0 +1,6 @@ +# DockStatAPI Dependency Graph + +```mermaid +graph TD + "index.ts" --> "routes/stacks.ts" +``` From 9139b03b72878c38b597f76a61bcd1452f170a76 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 13 Mar 2025 23:23:13 +0100 Subject: [PATCH 172/324] Fix: Switch to dependency-cruiser --- .github/scripts/generate-mermaid.js | 25 ------------------------- .github/workflows/dependency-graph.yml | 24 ++++++++++++++---------- 2 files changed, 14 insertions(+), 35 deletions(-) delete mode 100644 .github/scripts/generate-mermaid.js diff --git a/.github/scripts/generate-mermaid.js b/.github/scripts/generate-mermaid.js deleted file mode 100644 index d030db1..0000000 --- a/.github/scripts/generate-mermaid.js +++ /dev/null @@ -1,25 +0,0 @@ -const fs = require('fs'); -const path = require('path'); - -const dependencies = JSON.parse(fs.readFileSync('dependencies.json', 'utf-8')); - -const edges = []; -Object.entries(dependencies).forEach(([file, deps]) => { - deps.forEach(dep => { - edges.push(` "${file}" --> "${dep}"`); - }); -}); - -const mermaidContent = `graph TD -${edges.join('\n')}`; - -const markdownDoc = `# DockStatAPI Dependency Graph - -\`\`\`mermaid -${mermaidContent} -\`\`\` -`; - -fs.writeFileSync(path.join("./", 'dependencies.md'), markdownDoc); - -console.log('Successfully generated dependency graph'); \ No newline at end of file diff --git a/.github/workflows/dependency-graph.yml b/.github/workflows/dependency-graph.yml index 86d8e36..2a87e56 100644 --- a/.github/workflows/dependency-graph.yml +++ b/.github/workflows/dependency-graph.yml @@ -11,27 +11,31 @@ jobs: steps: - name: Checkout Repository uses: actions/checkout@v4 - with: - fetch-depth: 0 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 - - name: Install madge - run: npm install -g madge + - name: Install dependency-cruiser and graphviz + run: | + npm install -g dependency-cruiser + sudo apt-get install -y graphviz - - name: Generate Dependency Data - run: madge --json src/index.ts > dependencies.json + - name: Generate Mermaid Diagram + run: | + npx depcruise src --include-only "^src" --output-type mermaid > dependency-graph.mmd + echo "Mermaid diagram generated at dependency-graph.mmd" - - name: Generate Mermaid Markdown - run: node .github/scripts/generate-mermaid.js + - name: Generate SVG Dependency Graph + run: | + npx depcruise src --include-only "^src" --output-type dot | dot -T svg > dependency-graph.svg + echo "SVG graph generated at docs/dependency-graph.svg" - name: Commit and Push Changes uses: EndBug/add-and-commit@v9 with: - add: "dependencies.md" - message: "CI/CD: Update dependency graph" + add: "docs/*" + message: "Update dependency graphs" committer_name: "GitHub Action" committer_email: "action@github.com" From 8066901a7c0e80b4e4375a2e2e40d3d08a17a537 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 13 Mar 2025 23:23:37 +0100 Subject: [PATCH 173/324] Fix: Adjust pathing --- .github/workflows/dependency-graph.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependency-graph.yml b/.github/workflows/dependency-graph.yml index 2a87e56..be00d51 100644 --- a/.github/workflows/dependency-graph.yml +++ b/.github/workflows/dependency-graph.yml @@ -35,7 +35,7 @@ jobs: - name: Commit and Push Changes uses: EndBug/add-and-commit@v9 with: - add: "docs/*" + add: "dependency-graph*" message: "Update dependency graphs" committer_name: "GitHub Action" committer_email: "action@github.com" From 45579b08cbe526736435a90d550b6dd42c4fb067 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 13 Mar 2025 23:24:25 +0100 Subject: [PATCH 174/324] Fix: Adjust to no config --- .github/workflows/dependency-graph.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dependency-graph.yml b/.github/workflows/dependency-graph.yml index be00d51..256bf26 100644 --- a/.github/workflows/dependency-graph.yml +++ b/.github/workflows/dependency-graph.yml @@ -24,12 +24,12 @@ jobs: - name: Generate Mermaid Diagram run: | - npx depcruise src --include-only "^src" --output-type mermaid > dependency-graph.mmd + npx depcruise src --no-config --include-only "^src" --output-type mermaid > dependency-graph.mmd echo "Mermaid diagram generated at dependency-graph.mmd" - name: Generate SVG Dependency Graph run: | - npx depcruise src --include-only "^src" --output-type dot | dot -T svg > dependency-graph.svg + npx depcruise src --no-config --include-only "^src" --output-type dot | dot -T svg > dependency-graph.svg echo "SVG graph generated at docs/dependency-graph.svg" - name: Commit and Push Changes From afc46f24a8043ea3d37ce4ea2b411a47601bdde0 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Thu, 13 Mar 2025 22:24:52 +0000 Subject: [PATCH 175/324] Update dependency graphs --- dependency-graph.mmd | 4 ++++ dependency-graph.svg | 13 +++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 dependency-graph.mmd create mode 100644 dependency-graph.svg diff --git a/dependency-graph.mmd b/dependency-graph.mmd new file mode 100644 index 0000000..b4f50c2 --- /dev/null +++ b/dependency-graph.mmd @@ -0,0 +1,4 @@ +flowchart LR + + + diff --git a/dependency-graph.svg b/dependency-graph.svg new file mode 100644 index 0000000..b27cd79 --- /dev/null +++ b/dependency-graph.svg @@ -0,0 +1,13 @@ + + + + + + +dependency-cruiser output + + + From 5e4a12f6e5af042ed64cdfbe15c5d77baf820697 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 13 Mar 2025 23:28:28 +0100 Subject: [PATCH 176/324] Fix: Chhhhange to bun run --- .github/workflows/dependency-graph.yml | 10 +-- bun.lock | 105 +++++++++++++++++++++++-- dependency-graph.mmd | Bin 0 -> 2660 bytes package.json | 1 + 4 files changed, 102 insertions(+), 14 deletions(-) create mode 100644 dependency-graph.mmd diff --git a/.github/workflows/dependency-graph.yml b/.github/workflows/dependency-graph.yml index 256bf26..8631090 100644 --- a/.github/workflows/dependency-graph.yml +++ b/.github/workflows/dependency-graph.yml @@ -13,23 +13,21 @@ jobs: uses: actions/checkout@v4 - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 + uses: oven-sh/setup-bun@v2 - name: Install dependency-cruiser and graphviz run: | - npm install -g dependency-cruiser + bun add dependency-cruiser sudo apt-get install -y graphviz - name: Generate Mermaid Diagram run: | - npx depcruise src --no-config --include-only "^src" --output-type mermaid > dependency-graph.mmd + bun run depcruise src --no-config --include-only "^src" --output-type mermaid > dependency-graph.mmd echo "Mermaid diagram generated at dependency-graph.mmd" - name: Generate SVG Dependency Graph run: | - npx depcruise src --no-config --include-only "^src" --output-type dot | dot -T svg > dependency-graph.svg + bun run depcruise src --no-config --include-only "^src" --output-type dot | dot -T svg > dependency-graph.svg echo "SVG graph generated at docs/dependency-graph.svg" - name: Commit and Push Changes diff --git a/bun.lock b/bun.lock index 5ab6675..c9f06ca 100644 --- a/bun.lock +++ b/bun.lock @@ -24,6 +24,7 @@ "@types/split2": "^4.2.3", "bun-types": "latest", "cross-env": "^7.0.3", + "dependency-cruiser": "^16.10.0", "knip": "^5.46.0", "typescript": "^5.8.2", "wrap-ansi": "^9.0.0", @@ -110,8 +111,20 @@ "@unhead/schema": ["@unhead/schema@1.11.19", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-7VhYHWK7xHgljdv+C01MepCSYZO2v6OhgsfKWPxRQBDDGfUKCUaChox0XMq3tFvXP6u4zSp6yzcDw2yxCfVMwg=="], + "acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "acorn-jsx-walk": ["acorn-jsx-walk@2.0.0", "", {}, "sha512-uuo6iJj4D4ygkdzd6jPtcxs8vZgDX9YFIkqczGImoypX2fQ4dVImmu3UzA4ynixCIMTrEOWW+95M2HuBaCEOVA=="], + + "acorn-loose": ["acorn-loose@8.4.0", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-M0EUka6rb+QC4l9Z3T0nJEzNOO7JcoJlYMrBlyBCiFSXRyxjLKayd4TbQs2FDRWQU1h9FR7QVNHt+PEaoNL5rQ=="], + + "acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="], + "aggregate-error": ["aggregate-error@3.1.0", "", { "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" } }, "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA=="], + "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], @@ -148,15 +161,15 @@ "color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="], - "color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - "color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], "colorspace": ["colorspace@1.1.4", "", { "dependencies": { "color": "^3.1.3", "text-hex": "1.0.x" } }, "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w=="], - "commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + "commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="], "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], @@ -170,6 +183,8 @@ "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], + "dependency-cruiser": ["dependency-cruiser@16.10.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "acorn-jsx-walk": "^2.0.0", "acorn-loose": "^8.4.0", "acorn-walk": "^8.3.4", "ajv": "^8.17.1", "commander": "^13.1.0", "enhanced-resolve": "^5.18.1", "ignore": "^7.0.3", "interpret": "^3.1.1", "is-installed-globally": "^1.0.0", "json5": "^2.2.3", "memoize": "^10.0.0", "picocolors": "^1.1.1", "picomatch": "^4.0.2", "prompts": "^2.4.2", "rechoir": "^0.8.0", "safe-regex": "^2.1.1", "semver": "^7.7.1", "teamcity-service-messages": "^0.1.14", "tsconfig-paths-webpack-plugin": "^4.2.0", "watskeburt": "^4.2.3" }, "bin": { "dependency-cruiser": "bin/dependency-cruise.mjs", "dependency-cruise": "bin/dependency-cruise.mjs", "depcruise": "bin/dependency-cruise.mjs", "depcruise-baseline": "bin/depcruise-baseline.mjs", "depcruise-fmt": "bin/depcruise-fmt.mjs", "depcruise-wrap-stream-in-html": "bin/wrap-stream-in-html.mjs" } }, "sha512-o6pEB8X/XS0AjpQBhPJW3pSY7HIviRM7+G601T9ruV63NVJC4DxLMA+a1VzZlKOzO2fO6JKRHjRmGjzZZHEFYA=="], + "docker-compose": ["docker-compose@1.1.1", "", { "dependencies": { "yaml": "^2.2.2" } }, "sha512-UkIUz0LtzuO17Ijm6SXMGtfZMs7IvbNwvuJBiBuN93PIhr/n9/sbJMqpvYFaCBGfwu1ZM4PPPDgQzeeke4lEoA=="], "docker-modem": ["docker-modem@5.0.6", "", { "dependencies": { "debug": "^4.1.1", "readable-stream": "^3.5.0", "split-ca": "^1.0.1", "ssh2": "^1.15.0" } }, "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ=="], @@ -190,8 +205,12 @@ "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + "fast-uri": ["fast-uri@3.0.6", "", {}, "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw=="], + "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], "fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="], @@ -202,34 +221,52 @@ "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], "get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="], "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "global-directory": ["global-directory@4.0.1", "", { "dependencies": { "ini": "4.1.1" } }, "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q=="], + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "ignore": ["ignore@7.0.3", "", {}, "sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA=="], "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + "ini": ["ini@4.1.1", "", {}, "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g=="], + + "interpret": ["interpret@3.1.1", "", {}, "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ=="], + "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + "is-installed-globally": ["is-installed-globally@1.0.0", "", { "dependencies": { "global-directory": "^4.0.1", "is-path-inside": "^4.0.0" } }, "sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ=="], + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + "is-path-inside": ["is-path-inside@4.0.0", "", {}, "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA=="], + "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], @@ -238,6 +275,12 @@ "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], + "knip": ["knip@5.46.0", "", { "dependencies": { "@nodelib/fs.walk": "3.0.1", "@snyk/github-codeowners": "1.1.0", "easy-table": "1.2.0", "enhanced-resolve": "^5.18.0", "fast-glob": "^3.3.3", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "minimist": "^1.2.8", "picocolors": "^1.1.0", "picomatch": "^4.0.1", "pretty-ms": "^9.0.0", "smol-toml": "^1.3.1", "strip-json-comments": "5.0.1", "summary": "2.1.0", "zod": "^3.22.4", "zod-validation-error": "^3.0.3" }, "peerDependencies": { "@types/node": ">=18", "typescript": ">=5.0.4" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-WedHSK5xNBWYgm64Rt5B9b0CVXL2kRBcyCeet3NHgdv9en3QE4AWSDPEiX48NoPUBW3h//9S0VwLF5MG/MPi3g=="], "kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="], @@ -250,10 +293,14 @@ "memoirist": ["memoirist@0.3.0", "", {}, "sha512-wR+4chMgVPq+T6OOsk40u9Wlpw1Pjx66NMNiYxCQQ4EUJ7jDs3D9kTCeKdBOkvAiqXlHLVJlvYL01PvIJ1MPNg=="], + "memoize": ["memoize@10.1.0", "", { "dependencies": { "mimic-function": "^5.0.1" } }, "sha512-MMbFhJzh4Jlg/poq1si90XRlTZRDHVqdlz2mPyGJ6kqMpyHUyVpDd5gpFAvVehW64+RA1eKE9Yt8aSLY7w2Kgg=="], + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], @@ -278,6 +325,8 @@ "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], @@ -286,6 +335,8 @@ "pretty-ms": ["pretty-ms@9.2.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg=="], + "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], + "protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], "pump": ["pump@3.0.2", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw=="], @@ -296,24 +347,38 @@ "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "rechoir": ["rechoir@0.8.0", "", { "dependencies": { "resolve": "^1.20.0" } }, "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ=="], + + "regexp-tree": ["regexp-tree@0.1.27", "", { "bin": { "regexp-tree": "bin/regexp-tree" } }, "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "safe-regex": ["safe-regex@2.1.1", "", { "dependencies": { "regexp-tree": "~0.1.1" } }, "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A=="], + "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + "smol-toml": ["smol-toml@1.3.1", "", {}, "sha512-tEYNll18pPKHroYSmLLrksq233j021G0giwW7P3D24jC54pQ5W5BXMsQ/Mvw1OJCmEYDgY+lrzT+3nNUtoNfXQ=="], "split-ca": ["split-ca@1.0.1", "", {}, "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ=="], @@ -330,22 +395,34 @@ "strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], + "strip-json-comments": ["strip-json-comments@5.0.1", "", {}, "sha512-0fk9zBqO67Nq5M/m45qHCJxylV/DhBlIOVExqgOMiCCrzrhU6tCibRXNqE3jwJLftzE9SNuZtYbpzcO+i9FiKw=="], "summary": ["summary@2.1.0", "", {}, "sha512-nMIjMrd5Z2nuB2RZCKJfFMjgS3fygbeyGk9PxPPaJR1RIcyN9yn4A63Isovzm3ZtQuEkLBVgMdPup8UeLH7aQw=="], + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + "tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="], "tar-fs": ["tar-fs@2.0.1", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.0.0" } }, "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA=="], "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + "teamcity-service-messages": ["teamcity-service-messages@0.1.14", "", {}, "sha512-29aQwaHqm8RMX74u2o/h1KbMLP89FjNiMxD9wbF2BbWOnbM+q+d1sCEC+MqCc4QW3NJykn77OMpTFw/xTHIc0w=="], + "text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], "triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="], + "tsconfig-paths": ["tsconfig-paths@4.2.0", "", { "dependencies": { "json5": "^2.2.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg=="], + + "tsconfig-paths-webpack-plugin": ["tsconfig-paths-webpack-plugin@4.2.0", "", { "dependencies": { "chalk": "^4.1.0", "enhanced-resolve": "^5.7.0", "tapable": "^2.2.1", "tsconfig-paths": "^4.1.2" } }, "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA=="], + "tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="], "typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], @@ -356,6 +433,8 @@ "uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], + "watskeburt": ["watskeburt@4.2.3", "", { "bin": { "watskeburt": "dist/run-cli.js" } }, "sha512-uG9qtQYoHqAsnT711nG5iZc/8M5inSmkGCOp7pFaytKG2aTfIca7p//CjiVzAE4P7hzaYuCozMjNNaLgmhbK5g=="], + "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], @@ -386,6 +465,10 @@ "@scalar/themes/@scalar/types": ["@scalar/types@0.0.34", "", { "dependencies": { "@scalar/openapi-types": "0.1.8", "@unhead/schema": "^1.11.11" } }, "sha512-q01ctijmHArM5KOny2zU+sHfhpsgOAENrDENecK2TsQNn5FYLmFZouMKeW2M6F7KFLPZnFxUiL/rT88b6Rp/Kg=="], + "@snyk/github-codeowners/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + + "@snyk/github-codeowners/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "@types/docker-modem/@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], "@types/dockerode/@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], @@ -404,6 +487,10 @@ "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "color/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + + "color-string/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + "defaults/clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], "fast-glob/@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], @@ -414,6 +501,8 @@ "strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + "tsconfig-paths-webpack-plugin/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.1.8", "", {}, "sha512-iufA5/6hPCmRIVD2eh7qGpoKvoA08Gw/qUb2JECifBtAwA93fo7+1k9uHK440f2LMJsbxIzA+nv7RS0BmfiO/g=="], @@ -424,14 +513,14 @@ "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "color/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + "fast-glob/@nodelib/fs.walk/@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + "tsconfig-paths-webpack-plugin/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "cliui/wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - - "cliui/wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], } } diff --git a/dependency-graph.mmd b/dependency-graph.mmd new file mode 100644 index 0000000000000000000000000000000000000000..4602d0bdc0d7b3d492e30063fae90caf31a09bc7 GIT binary patch literal 2660 zcmb7GU2oG+4D~Y-|6$_2h)`O_3lgw(jFqk*z!->!H0|0#)(jDDXLQ9HiLt8qcK8@)iJ-{cY9ZhM$nwT=i9MKc{N{=Y#Hwiz# z?LkFACG2}c^9I~g;AZT7%bzKK@)Euc?ULW59DUBd9y2cg=dTOBM94T&PAk?*#9tNM zoYpkklj$?~*sQ!H9siOAA#TkGqJmGlBznF3*5`&FITvl_hgSEjFS2AcAI3t!5T^vP#^4vm~X z;dH~#^uK4|9ejv&#`zWfzrb7JNZ9yU=}aG5i6`eNWGmbz?_rT4$Am3pVswg&hyx&G z`>b%bq^2AGzTnuQot=~+Y|C>TcIS|fQH_uCWFE2~!(R7@O!6W81o(1S58L=DxrBAm zHOq!a`EOUi@9SsSug}2y4*QwAuF1Q+t0U4~U*AcK)V<%_H*29=+**{oWYN0YE&86P zZ^~z=*m>2>7LndIzLRC2p3#uAFJPvAzeMgnm9rG{<`C2zQ{uOg?P5fFx61hvtlrJH zjW$i$MWow)%eSVSF}cfpS0mDmU%jELIqBZFjLQCziexuSO=Z3!(mSARQAOyUFC*@- zb*i{6p4k*=I{4WEAjpCy^G literal 0 HcmV?d00001 diff --git a/package.json b/package.json index 55a9e43..da50177 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@types/split2": "^4.2.3", "bun-types": "latest", "cross-env": "^7.0.3", + "dependency-cruiser": "^16.10.0", "knip": "^5.46.0", "typescript": "^5.8.2", "wrap-ansi": "^9.0.0" From bede27673a3dc28205951ef552af15fd91f5ac4c Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 13 Mar 2025 23:30:49 +0100 Subject: [PATCH 177/324] Fix: Change to Bun run --- dependencies.md | 6 ------ dependency-graph.svg | 13 ------------- 2 files changed, 19 deletions(-) delete mode 100644 dependencies.md delete mode 100644 dependency-graph.svg diff --git a/dependencies.md b/dependencies.md deleted file mode 100644 index fbb7268..0000000 --- a/dependencies.md +++ /dev/null @@ -1,6 +0,0 @@ -# DockStatAPI Dependency Graph - -```mermaid -graph TD - "index.ts" --> "routes/stacks.ts" -``` diff --git a/dependency-graph.svg b/dependency-graph.svg deleted file mode 100644 index b27cd79..0000000 --- a/dependency-graph.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - -dependency-cruiser output - - - From af85b57fc1db535d85d0fde78e13ea5bd7d996db Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Thu, 13 Mar 2025 22:31:34 +0000 Subject: [PATCH 178/324] Update dependency graphs --- dependency-graph.mmd | 87 +++++++ dependency-graph.svg | 559 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 646 insertions(+) create mode 100644 dependency-graph.mmd create mode 100644 dependency-graph.svg diff --git a/dependency-graph.mmd b/dependency-graph.mmd new file mode 100644 index 0000000..2aa02d5 --- /dev/null +++ b/dependency-graph.mmd @@ -0,0 +1,87 @@ +flowchart LR + +subgraph 0["src"] +subgraph 1["core"] +subgraph 2["database"] +3["helper.ts"] +6["repository.ts"] +end +subgraph 4["utils"] +5["logger.ts"] +E["change-me-checker.ts"] +T["calculations.ts"] +U["package-json.ts"] +V["respone-handler.ts"] +end +subgraph 7["docker"] +8["client.ts"] +9["scheduler.ts"] +A["store-container-stats.ts"] +B["store-host-stats.ts"] +end +subgraph C["plugins"] +D["loader.ts"] +F["plugin-manager.ts"] +G["plugin-actions.ts"] +end +subgraph H["stacks"] +I["controller.ts"] +end +subgraph J["trpc"] +K["index.ts"] +L["router.ts"] +subgraph M["procedures"] +N["api-config.procedure.ts"] +P["docker-manager.procedure.ts"] +Q["docker-stats.procedure.ts"] +R["logs.procedure.ts"] +S["stacks.procedure.ts"] +end +O["trpc.ts"] +end +end +W["index.ts"] +subgraph X["routes"] +Y["stacks.ts"] +12["api-config.ts"] +13["docker-manager.ts"] +14["docker-stats.ts"] +15["docker-websocket.ts"] +16["logs.ts"] +end +subgraph Z["plugins"] +10["example.plugin.ts"] +11["telegram.plugin.ts"] +end +subgraph 17["typings"] +18["database.ts"] +19["docker-compose.ts"] +1A["docker.ts"] +1B["dockerode.ts"] +1C["plugin.ts"] +1D["websocket.ts"] +end +end +3-->5 +5-->6 +6-->3 +D-->E +D-->5 +D-->F +F-->5 +G-->F +I-->6 +I-->5 +K-->L +L-->N +L-->P +L-->Q +L-->R +L-->S +L-->O +N-->O +P-->O +Q-->O +R-->O +S-->O +W-->Y diff --git a/dependency-graph.svg b/dependency-graph.svg new file mode 100644 index 0000000..fe7fa83 --- /dev/null +++ b/dependency-graph.svg @@ -0,0 +1,559 @@ + + + + + + +dependency-cruiser output + + +cluster_src + +src + + +cluster_src/core + +core + + +cluster_src/core/database + +database + + +cluster_src/core/docker + +docker + + +cluster_src/core/plugins + +plugins + + +cluster_src/core/stacks + +stacks + + +cluster_src/core/trpc + +trpc + + +cluster_src/core/trpc/procedures + +procedures + + +cluster_src/core/utils + +utils + + +cluster_src/plugins + +plugins + + +cluster_src/routes + +routes + + +cluster_src/typings + +typings + + + +src/core/database/helper.ts + + +helper.ts + + + + + +src/core/utils/logger.ts + + +logger.ts + + + + + +src/core/database/helper.ts->src/core/utils/logger.ts + + + + + + + +src/core/database/repository.ts + + +repository.ts + + + + + +src/core/utils/logger.ts->src/core/database/repository.ts + + + + + + + +src/core/database/repository.ts->src/core/database/helper.ts + + + + + + + +src/core/docker/client.ts + + +client.ts + + + + + +src/core/docker/scheduler.ts + + +scheduler.ts + + + + + +src/core/docker/store-container-stats.ts + + +store-container-stats.ts + + + + + +src/core/docker/store-host-stats.ts + + +store-host-stats.ts + + + + + +src/core/plugins/loader.ts + + +loader.ts + + + + + +src/core/plugins/loader.ts->src/core/utils/logger.ts + + + + + +src/core/utils/change-me-checker.ts + + +change-me-checker.ts + + + + + +src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts + + + + + +src/core/plugins/plugin-manager.ts + + +plugin-manager.ts + + + + + +src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts + + + + + +src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts + + + + + +src/core/plugins/plugin-actions.ts + + +plugin-actions.ts + + + + + +src/core/plugins/plugin-actions.ts->src/core/plugins/plugin-manager.ts + + + + + +src/core/stacks/controller.ts + + +controller.ts + + + + + +src/core/stacks/controller.ts->src/core/utils/logger.ts + + + + + +src/core/stacks/controller.ts->src/core/database/repository.ts + + + + + +src/core/trpc/index.ts + + +index.ts + + + + + +src/core/trpc/router.ts + + +router.ts + + + + + +src/core/trpc/index.ts->src/core/trpc/router.ts + + + + + +src/core/trpc/procedures/api-config.procedure.ts + + +api-config.procedure.ts + + + + + +src/core/trpc/router.ts->src/core/trpc/procedures/api-config.procedure.ts + + + + + +src/core/trpc/trpc.ts + + +trpc.ts + + + + + +src/core/trpc/router.ts->src/core/trpc/trpc.ts + + + + + +src/core/trpc/procedures/docker-manager.procedure.ts + + +docker-manager.procedure.ts + + + + + +src/core/trpc/router.ts->src/core/trpc/procedures/docker-manager.procedure.ts + + + + + +src/core/trpc/procedures/docker-stats.procedure.ts + + +docker-stats.procedure.ts + + + + + +src/core/trpc/router.ts->src/core/trpc/procedures/docker-stats.procedure.ts + + + + + +src/core/trpc/procedures/logs.procedure.ts + + +logs.procedure.ts + + + + + +src/core/trpc/router.ts->src/core/trpc/procedures/logs.procedure.ts + + + + + +src/core/trpc/procedures/stacks.procedure.ts + + +stacks.procedure.ts + + + + + +src/core/trpc/router.ts->src/core/trpc/procedures/stacks.procedure.ts + + + + + +src/core/trpc/procedures/api-config.procedure.ts->src/core/trpc/trpc.ts + + + + + +src/core/trpc/procedures/docker-manager.procedure.ts->src/core/trpc/trpc.ts + + + + + +src/core/trpc/procedures/docker-stats.procedure.ts->src/core/trpc/trpc.ts + + + + + +src/core/trpc/procedures/logs.procedure.ts->src/core/trpc/trpc.ts + + + + + +src/core/trpc/procedures/stacks.procedure.ts->src/core/trpc/trpc.ts + + + + + +src/core/utils/calculations.ts + + +calculations.ts + + + + + +src/core/utils/package-json.ts + + +package-json.ts + + + + + +src/core/utils/respone-handler.ts + + +respone-handler.ts + + + + + +src/index.ts + + +index.ts + + + + + +src/routes/stacks.ts + + +stacks.ts + + + + + +src/index.ts->src/routes/stacks.ts + + + + + +src/plugins/example.plugin.ts + + +example.plugin.ts + + + + + +src/plugins/telegram.plugin.ts + + +telegram.plugin.ts + + + + + +src/routes/api-config.ts + + +api-config.ts + + + + + +src/routes/docker-manager.ts + + +docker-manager.ts + + + + + +src/routes/docker-stats.ts + + +docker-stats.ts + + + + + +src/routes/docker-websocket.ts + + +docker-websocket.ts + + + + + +src/routes/logs.ts + + +logs.ts + + + + + +src/typings/database.ts + + +database.ts + + + + + +src/typings/docker-compose.ts + + +docker-compose.ts + + + + + +src/typings/docker.ts + + +docker.ts + + + + + +src/typings/dockerode.ts + + +dockerode.ts + + + + + +src/typings/plugin.ts + + +plugin.ts + + + + + +src/typings/websocket.ts + + +websocket.ts + + + + + From b1ff316ce84dac9dc3b3724b29e26f6819161c3a Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 13 Mar 2025 23:55:46 +0100 Subject: [PATCH 179/324] Fix: Adjust Workflow to only render svg --- .github/workflows/dependency-graph.yml | 7 +-- dependency-graph.mmd | 87 -------------------------- 2 files changed, 1 insertion(+), 93 deletions(-) delete mode 100644 dependency-graph.mmd diff --git a/.github/workflows/dependency-graph.yml b/.github/workflows/dependency-graph.yml index 8631090..9398d2b 100644 --- a/.github/workflows/dependency-graph.yml +++ b/.github/workflows/dependency-graph.yml @@ -20,14 +20,9 @@ jobs: bun add dependency-cruiser sudo apt-get install -y graphviz - - name: Generate Mermaid Diagram - run: | - bun run depcruise src --no-config --include-only "^src" --output-type mermaid > dependency-graph.mmd - echo "Mermaid diagram generated at dependency-graph.mmd" - - name: Generate SVG Dependency Graph run: | - bun run depcruise src --no-config --include-only "^src" --output-type dot | dot -T svg > dependency-graph.svg + bun run dependency-cruiser --output-type ddot src/index.ts --no-config -x node_modules --ts-pre-compilation-deps --ts-config tsconfig.json | dot -T svg > dependency-graph.svg echo "SVG graph generated at docs/dependency-graph.svg" - name: Commit and Push Changes diff --git a/dependency-graph.mmd b/dependency-graph.mmd deleted file mode 100644 index 2aa02d5..0000000 --- a/dependency-graph.mmd +++ /dev/null @@ -1,87 +0,0 @@ -flowchart LR - -subgraph 0["src"] -subgraph 1["core"] -subgraph 2["database"] -3["helper.ts"] -6["repository.ts"] -end -subgraph 4["utils"] -5["logger.ts"] -E["change-me-checker.ts"] -T["calculations.ts"] -U["package-json.ts"] -V["respone-handler.ts"] -end -subgraph 7["docker"] -8["client.ts"] -9["scheduler.ts"] -A["store-container-stats.ts"] -B["store-host-stats.ts"] -end -subgraph C["plugins"] -D["loader.ts"] -F["plugin-manager.ts"] -G["plugin-actions.ts"] -end -subgraph H["stacks"] -I["controller.ts"] -end -subgraph J["trpc"] -K["index.ts"] -L["router.ts"] -subgraph M["procedures"] -N["api-config.procedure.ts"] -P["docker-manager.procedure.ts"] -Q["docker-stats.procedure.ts"] -R["logs.procedure.ts"] -S["stacks.procedure.ts"] -end -O["trpc.ts"] -end -end -W["index.ts"] -subgraph X["routes"] -Y["stacks.ts"] -12["api-config.ts"] -13["docker-manager.ts"] -14["docker-stats.ts"] -15["docker-websocket.ts"] -16["logs.ts"] -end -subgraph Z["plugins"] -10["example.plugin.ts"] -11["telegram.plugin.ts"] -end -subgraph 17["typings"] -18["database.ts"] -19["docker-compose.ts"] -1A["docker.ts"] -1B["dockerode.ts"] -1C["plugin.ts"] -1D["websocket.ts"] -end -end -3-->5 -5-->6 -6-->3 -D-->E -D-->5 -D-->F -F-->5 -G-->F -I-->6 -I-->5 -K-->L -L-->N -L-->P -L-->Q -L-->R -L-->S -L-->O -N-->O -P-->O -Q-->O -R-->O -S-->O -W-->Y From 14c4240cd7e7b9e7e706da16741ba9d5790a87ae Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Thu, 13 Mar 2025 22:56:11 +0000 Subject: [PATCH 180/324] Update dependency graphs --- dependency-graph.svg | 762 ++++++++++++++++--------------------------- 1 file changed, 284 insertions(+), 478 deletions(-) diff --git a/dependency-graph.svg b/dependency-graph.svg index fe7fa83..144a9bf 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -4,556 +4,362 @@ - - + + dependency-cruiser output - + cluster_src - -src + +src cluster_src/core - -core + +core -cluster_src/core/database - -database - - -cluster_src/core/docker - -docker - - -cluster_src/core/plugins - -plugins - - -cluster_src/core/stacks - -stacks - - cluster_src/core/trpc - -trpc - - -cluster_src/core/trpc/procedures - -procedures - - -cluster_src/core/utils - -utils - - -cluster_src/plugins - -plugins - - -cluster_src/routes - -routes - - -cluster_src/typings - -typings - - + +trpc + + -src/core/database/helper.ts - - -helper.ts +. + + + + + +. - + -src/core/utils/logger.ts - - -logger.ts - - - - - -src/core/database/helper.ts->src/core/utils/logger.ts - - - - - - - -src/core/database/repository.ts - - -repository.ts +fs + + + + + +fs - - -src/core/utils/logger.ts->src/core/database/repository.ts - - - - - - - -src/core/database/repository.ts->src/core/database/helper.ts - - - - - - + + -src/core/docker/client.ts - - -client.ts +src/routes + + + + + +routes - + + +src->src/routes + + + + -src/core/docker/scheduler.ts - - -scheduler.ts +src/core/database + + + + + +database - + + +src->src/core/database + + + + -src/core/docker/store-container-stats.ts - - -store-container-stats.ts +src/core/docker + + + + + +docker - - -src/core/docker/store-host-stats.ts - - -store-host-stats.ts - - + + +src->src/core/docker + + - - -src/core/plugins/loader.ts - - -loader.ts + + +src/core/plugins + + + + + +plugins - + -src/core/plugins/loader.ts->src/core/utils/logger.ts - - - - - -src/core/utils/change-me-checker.ts - - -change-me-checker.ts - +src->src/core/plugins + + + + + +src->src/core/trpc + + - - -src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts - - - - - -src/core/plugins/plugin-manager.ts - - -plugin-manager.ts + + +src/core/utils + + + + + +utils - - -src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts - - - - - -src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts - - - - + + +src->src/core/utils + + + + + +src/routes->. + + + + + +src/routes->src/core/database + + + + + +src/routes->src/core/docker + + + + + +src/routes->src/core/utils + + + + -src/core/plugins/plugin-actions.ts - - -plugin-actions.ts +src/typings + + + + + +typings - - -src/core/plugins/plugin-actions.ts->src/core/plugins/plugin-manager.ts - - + + +src/routes->src/typings + + - + -src/core/stacks/controller.ts - - -controller.ts +src/core/stacks + + + + + +stacks - - -src/core/stacks/controller.ts->src/core/utils/logger.ts - - - - - -src/core/stacks/controller.ts->src/core/database/repository.ts - - - - - -src/core/trpc/index.ts - - -index.ts - + + +src/routes->src/core/stacks + + + + +src/core/database->. + + - - -src/core/trpc/router.ts - - -router.ts - + + +src/core/database->src/core/utils + + + + + + +src/core/database->src/typings + + - + -src/core/trpc/index.ts->src/core/trpc/router.ts - - - - - -src/core/trpc/procedures/api-config.procedure.ts - - -api-config.procedure.ts - - - - - -src/core/trpc/router.ts->src/core/trpc/procedures/api-config.procedure.ts - - - - - -src/core/trpc/trpc.ts - - -trpc.ts - - - - - -src/core/trpc/router.ts->src/core/trpc/trpc.ts - - - - - -src/core/trpc/procedures/docker-manager.procedure.ts - - -docker-manager.procedure.ts - - - - - -src/core/trpc/router.ts->src/core/trpc/procedures/docker-manager.procedure.ts - - - - - -src/core/trpc/procedures/docker-stats.procedure.ts - - -docker-stats.procedure.ts - - - - - -src/core/trpc/router.ts->src/core/trpc/procedures/docker-stats.procedure.ts - - - - - -src/core/trpc/procedures/logs.procedure.ts - - -logs.procedure.ts - - - - - -src/core/trpc/router.ts->src/core/trpc/procedures/logs.procedure.ts - - - - - -src/core/trpc/procedures/stacks.procedure.ts - - -stacks.procedure.ts - - - - - -src/core/trpc/router.ts->src/core/trpc/procedures/stacks.procedure.ts - - +src/core/docker->src/core/database + + - + -src/core/trpc/procedures/api-config.procedure.ts->src/core/trpc/trpc.ts - - +src/core/docker->src/core/utils + + - + -src/core/trpc/procedures/docker-manager.procedure.ts->src/core/trpc/trpc.ts - - +src/core/docker->src/typings + + - - -src/core/trpc/procedures/docker-stats.procedure.ts->src/core/trpc/trpc.ts - - - - + -src/core/trpc/procedures/logs.procedure.ts->src/core/trpc/trpc.ts - - - - - -src/core/trpc/procedures/stacks.procedure.ts->src/core/trpc/trpc.ts - - - - - -src/core/utils/calculations.ts - - -calculations.ts - - - - - -src/core/utils/package-json.ts - - -package-json.ts - - - - - -src/core/utils/respone-handler.ts - - -respone-handler.ts - - - - - -src/index.ts - - -index.ts - - - - - -src/routes/stacks.ts - - -stacks.ts - - - - - -src/index.ts->src/routes/stacks.ts - - - - - -src/plugins/example.plugin.ts - - -example.plugin.ts - - - - - -src/plugins/telegram.plugin.ts - - -telegram.plugin.ts - - - - - -src/routes/api-config.ts - - -api-config.ts - - - - - -src/routes/docker-manager.ts - - -docker-manager.ts - - - - - -src/routes/docker-stats.ts - - -docker-stats.ts - - +src/core/plugins->. + + - - -src/routes/docker-websocket.ts - - -docker-websocket.ts - - - - - -src/routes/logs.ts - - -logs.ts - - - - - -src/typings/database.ts - - -database.ts - + + +src/core/plugins->src/core/utils + + + + +src/core/plugins->src/typings + + - - -src/typings/docker-compose.ts - - -docker-compose.ts + + +src/core/trpc/procedures + + + + + +procedures - - -src/typings/docker.ts - - -docker.ts - + + +src/core/trpc->src/core/trpc/procedures + + + + + +src/core/utils->. + + + + + +src/core/utils->fs + + + + + +src/core/utils->src/core/database + + + + + + + + +src/typings->. + + + + + +src/core/stacks->src/core/database + + + + +src/core/stacks->src/core/utils + + - - -src/typings/dockerode.ts - - -dockerode.ts - + + +src/core/stacks->src/typings + + + + +src/core/trpc/procedures->src/core/database + + - - -src/typings/plugin.ts - - -plugin.ts - + + +src/core/trpc/procedures->src/core/docker + + + + +src/core/trpc/procedures->src/core/trpc + + - - -src/typings/websocket.ts - - -websocket.ts - + + +src/core/trpc/procedures->src/core/utils + + + + +src/core/trpc/procedures->src/typings + + + + + +src/core/trpc/procedures->src/core/stacks + + From 5996947afe899ff5de39e4cc338d2e5e91f8693a Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 14 Mar 2025 00:06:08 +0100 Subject: [PATCH 181/324] Fix: Adjust CI/CD --- .github/scripts/dep-graph.sh | 14 + .github/workflows/dependency-graph.yml | 16 +- dependency-graph.svg | 365 ------------------------- 3 files changed, 27 insertions(+), 368 deletions(-) create mode 100644 .github/scripts/dep-graph.sh delete mode 100644 dependency-graph.svg diff --git a/.github/scripts/dep-graph.sh b/.github/scripts/dep-graph.sh new file mode 100644 index 0000000..d702dc1 --- /dev/null +++ b/.github/scripts/dep-graph.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +mermaidContent="$(cat dependency-graph.mmd)" + +echo " +--- +config: + flowchart: + defaultRenderer: elk +--- + +$mermaidContent +" > dependency-graph.mmd + diff --git a/.github/workflows/dependency-graph.yml b/.github/workflows/dependency-graph.yml index 9398d2b..019e039 100644 --- a/.github/workflows/dependency-graph.yml +++ b/.github/workflows/dependency-graph.yml @@ -20,10 +20,20 @@ jobs: bun add dependency-cruiser sudo apt-get install -y graphviz - - name: Generate SVG Dependency Graph + - name: Generate Mermaid Dependency Graph run: | - bun run dependency-cruiser --output-type ddot src/index.ts --no-config -x node_modules --ts-pre-compilation-deps --ts-config tsconfig.json | dot -T svg > dependency-graph.svg - echo "SVG graph generated at docs/dependency-graph.svg" + bun run dependency-cruiser --output-type mermaid src/index.ts --output-to dependency-graph.mmd --no-config -x node_modules --ts-pre-compilation-deps --ts-config tsconfig.json + echo "Mermaid graph generated at dependency-graph.mmd" + + - name: Convert to ELK Layout + run: | + bash ./.github/scripts/dep-graph.sh + + - name: Generate Dependency Graph (SVG) + run: | + bun run dependency-cruiser --output-type dot src/index.ts --output-to dependency-graph.dot --no-config -x node_modules --ts-pre-compilation-deps --ts-config tsconfig.json + dot -Tsvg dependency-graph.dot -o dependency-graph.svg + echo "SVG graph generated at dependency-graph.svg" - name: Commit and Push Changes uses: EndBug/add-and-commit@v9 diff --git a/dependency-graph.svg b/dependency-graph.svg deleted file mode 100644 index 144a9bf..0000000 --- a/dependency-graph.svg +++ /dev/null @@ -1,365 +0,0 @@ - - - - - - -dependency-cruiser output - - -cluster_src - -src - - -cluster_src/core - -core - - -cluster_src/core/trpc - -trpc - - - -. - - - - - -. - - - - - -fs - - - - - -fs - - - - - - -src/routes - - - - - -routes - - - - - -src->src/routes - - - - - -src/core/database - - - - - -database - - - - - -src->src/core/database - - - - - -src/core/docker - - - - - -docker - - - - - -src->src/core/docker - - - - - -src/core/plugins - - - - - -plugins - - - - - -src->src/core/plugins - - - - - - -src->src/core/trpc - - - - - -src/core/utils - - - - - -utils - - - - - -src->src/core/utils - - - - - -src/routes->. - - - - - -src/routes->src/core/database - - - - - -src/routes->src/core/docker - - - - - -src/routes->src/core/utils - - - - - -src/typings - - - - - -typings - - - - - -src/routes->src/typings - - - - - -src/core/stacks - - - - - -stacks - - - - - -src/routes->src/core/stacks - - - - - -src/core/database->. - - - - - -src/core/database->src/core/utils - - - - - - - -src/core/database->src/typings - - - - - -src/core/docker->src/core/database - - - - - -src/core/docker->src/core/utils - - - - - -src/core/docker->src/typings - - - - - -src/core/plugins->. - - - - - -src/core/plugins->src/core/utils - - - - - -src/core/plugins->src/typings - - - - - -src/core/trpc/procedures - - - - - -procedures - - - - - -src/core/trpc->src/core/trpc/procedures - - - - - -src/core/utils->. - - - - - -src/core/utils->fs - - - - - -src/core/utils->src/core/database - - - - - - - - -src/typings->. - - - - - -src/core/stacks->src/core/database - - - - - -src/core/stacks->src/core/utils - - - - - -src/core/stacks->src/typings - - - - - -src/core/trpc/procedures->src/core/database - - - - - -src/core/trpc/procedures->src/core/docker - - - - - -src/core/trpc/procedures->src/core/trpc - - - - - -src/core/trpc/procedures->src/core/utils - - - - - -src/core/trpc/procedures->src/typings - - - - - -src/core/trpc/procedures->src/core/stacks - - - - - From 95e184447d0df4585470900d84ed7c80430757c6 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Thu, 13 Mar 2025 23:06:33 +0000 Subject: [PATCH 182/324] Update dependency graphs --- dependency-graph.dot | 159 ++++++ dependency-graph.mmd | 186 +++++++ dependency-graph.svg | 1125 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 1470 insertions(+) create mode 100644 dependency-graph.dot create mode 100644 dependency-graph.mmd create mode 100644 dependency-graph.svg diff --git a/dependency-graph.dot b/dependency-graph.dot new file mode 100644 index 0000000..4cf81c6 --- /dev/null +++ b/dependency-graph.dot @@ -0,0 +1,159 @@ +strict digraph "dependency-cruiser output"{ + rankdir="LR" splines="true" overlap="false" nodesep="0.16" ranksep="0.18" fontname="Helvetica-bold" fontsize="9" style="rounded,bold,filled" fillcolor="#ffffff" compound="true" + node [shape="box" style="rounded, filled" height="0.2" color="black" fillcolor="#ffffcc" fontcolor="black" fontname="Helvetica" fontsize="9"] + edge [arrowhead="normal" arrowsize="0.6" penwidth="2.0" color="#00000033" fontname="Helvetica" fontsize="9"] + + "bun:sqlite" [label= tooltip="bun:sqlite" ] + "events" [label= tooltip="events" URL="https://nodejs.org/api/events.html" color="grey" fontcolor="grey"] + "fs" [label= tooltip="fs" URL="https://nodejs.org/api/fs.html" color="grey" fontcolor="grey"] + subgraph "cluster_fs" {label="fs" "fs/promises" [label= tooltip="promises" URL="https://nodejs.org/api/fs.html" color="grey" fontcolor="grey"] } + "package.json" [label= tooltip="package.json" URL="package.json" fillcolor="#ffee44"] + "path" [label= tooltip="path" URL="https://nodejs.org/api/path.html" color="grey" fontcolor="grey"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/helper.ts" [label= tooltip="helper.ts" URL="src/core/database/helper.ts" fillcolor="#ddfeff"] } } } + "src/core/database/helper.ts" -> "src/core/utils/logger.ts" [arrowhead="normalnoneodot"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/repository.ts" [label= tooltip="repository.ts" URL="src/core/database/repository.ts" fillcolor="#ddfeff"] } } } + "src/core/database/repository.ts" -> "src/core/database/helper.ts" [arrowhead="normalnoneodot"] + "src/core/database/repository.ts" -> "src/core/utils/logger.ts" [arrowhead="normalnoneodot"] + "src/core/database/repository.ts" -> "src/typings/database.ts" [arrowhead="onormal" penwidth="1.0"] + "src/core/database/repository.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] + "src/core/database/repository.ts" -> "bun:sqlite" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/client.ts" [label= tooltip="client.ts" URL="src/core/docker/client.ts" fillcolor="#ddfeff"] } } } + "src/core/docker/client.ts" -> "src/core/utils/logger.ts" + "src/core/docker/client.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/scheduler.ts" [label= tooltip="scheduler.ts" URL="src/core/docker/scheduler.ts" fillcolor="#ddfeff"] } } } + "src/core/docker/scheduler.ts" -> "src/core/database/repository.ts" + "src/core/docker/scheduler.ts" -> "src/core/docker/store-host-stats.ts" + "src/core/docker/scheduler.ts" -> "src/core/docker/store-container-stats.ts" + "src/core/docker/scheduler.ts" -> "src/core/utils/logger.ts" + "src/core/docker/scheduler.ts" -> "src/typings/database.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/store-container-stats.ts" [label= tooltip="store-container-stats.ts" URL="src/core/docker/store-container-stats.ts" fillcolor="#ddfeff"] } } } + "src/core/docker/store-container-stats.ts" -> "src/core/database/repository.ts" + "src/core/docker/store-container-stats.ts" -> "src/core/docker/client.ts" + "src/core/docker/store-container-stats.ts" -> "src/core/utils/calculations.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/store-host-stats.ts" [label= tooltip="store-host-stats.ts" URL="src/core/docker/store-host-stats.ts" fillcolor="#ddfeff"] } } } + "src/core/docker/store-host-stats.ts" -> "src/core/database/repository.ts" + "src/core/docker/store-host-stats.ts" -> "src/core/docker/client.ts" + "src/core/docker/store-host-stats.ts" -> "src/core/utils/logger.ts" + "src/core/docker/store-host-stats.ts" -> "src/typings/docker.ts" + "src/core/docker/store-host-stats.ts" -> "src/typings/dockerode.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/plugins" {label="plugins" "src/core/plugins/loader.ts" [label= tooltip="loader.ts" URL="src/core/plugins/loader.ts" fillcolor="#ddfeff"] } } } + "src/core/plugins/loader.ts" -> "src/core/utils/change-me-checker.ts" + "src/core/plugins/loader.ts" -> "src/core/utils/logger.ts" + "src/core/plugins/loader.ts" -> "src/core/plugins/plugin-manager.ts" + "src/core/plugins/loader.ts" -> "fs" [style="dashed" penwidth="1.0"] + "src/core/plugins/loader.ts" -> "path" [style="dashed" penwidth="1.0"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/plugins" {label="plugins" "src/core/plugins/plugin-manager.ts" [label= tooltip="plugin-manager.ts" URL="src/core/plugins/plugin-manager.ts" fillcolor="#ddfeff"] } } } + "src/core/plugins/plugin-manager.ts" -> "src/core/utils/logger.ts" + "src/core/plugins/plugin-manager.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] + "src/core/plugins/plugin-manager.ts" -> "src/typings/plugin.ts" [arrowhead="onormal" penwidth="1.0"] + "src/core/plugins/plugin-manager.ts" -> "events" [style="dashed" penwidth="1.0"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/stacks" {label="stacks" "src/core/stacks/controller.ts" [label= tooltip="controller.ts" URL="src/core/stacks/controller.ts" fillcolor="#ddfeff"] } } } + "src/core/stacks/controller.ts" -> "src/core/database/repository.ts" + "src/core/stacks/controller.ts" -> "src/core/utils/logger.ts" + "src/core/stacks/controller.ts" -> "src/typings/database.ts" [arrowhead="onormal" penwidth="1.0"] + "src/core/stacks/controller.ts" -> "src/typings/docker-compose.ts" [arrowhead="onormal" penwidth="1.0"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" "src/core/trpc/index.ts" [label= tooltip="index.ts" URL="src/core/trpc/index.ts" fillcolor="#ddfeff"] } } } + "src/core/trpc/index.ts" -> "src/core/trpc/router.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/api-config.procedure.ts" [label= tooltip="api-config.procedure.ts" URL="src/core/trpc/procedures/api-config.procedure.ts" fillcolor="#ddfeff"] } } } } + "src/core/trpc/procedures/api-config.procedure.ts" -> "src/core/trpc/trpc.ts" + "src/core/trpc/procedures/api-config.procedure.ts" -> "src/core/database/repository.ts" + "src/core/trpc/procedures/api-config.procedure.ts" -> "src/core/utils/logger.ts" + "src/core/trpc/procedures/api-config.procedure.ts" -> "src/core/utils/package-json.ts" + "src/core/trpc/procedures/api-config.procedure.ts" -> "src/typings/database.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/docker-manager.procedure.ts" [label= tooltip="docker-manager.procedure.ts" URL="src/core/trpc/procedures/docker-manager.procedure.ts" fillcolor="#ddfeff"] } } } } + "src/core/trpc/procedures/docker-manager.procedure.ts" -> "src/core/trpc/trpc.ts" + "src/core/trpc/procedures/docker-manager.procedure.ts" -> "src/core/database/repository.ts" + "src/core/trpc/procedures/docker-manager.procedure.ts" -> "src/core/utils/logger.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/docker-stats.procedure.ts" [label= tooltip="docker-stats.procedure.ts" URL="src/core/trpc/procedures/docker-stats.procedure.ts" fillcolor="#ddfeff"] } } } } + "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/trpc/trpc.ts" + "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/database/repository.ts" + "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/docker/client.ts" + "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/utils/calculations.ts" + "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/utils/logger.ts" + "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] + "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/typings/dockerode.ts" [arrowhead="onormal" penwidth="1.0"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/logs.procedure.ts" [label= tooltip="logs.procedure.ts" URL="src/core/trpc/procedures/logs.procedure.ts" fillcolor="#ddfeff"] } } } } + "src/core/trpc/procedures/logs.procedure.ts" -> "src/core/trpc/trpc.ts" + "src/core/trpc/procedures/logs.procedure.ts" -> "src/core/database/repository.ts" + "src/core/trpc/procedures/logs.procedure.ts" -> "src/core/utils/logger.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/stacks.procedure.ts" [label= tooltip="stacks.procedure.ts" URL="src/core/trpc/procedures/stacks.procedure.ts" fillcolor="#ddfeff"] } } } } + "src/core/trpc/procedures/stacks.procedure.ts" -> "src/core/trpc/trpc.ts" + "src/core/trpc/procedures/stacks.procedure.ts" -> "src/core/database/repository.ts" + "src/core/trpc/procedures/stacks.procedure.ts" -> "src/core/stacks/controller.ts" + "src/core/trpc/procedures/stacks.procedure.ts" -> "src/core/utils/logger.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" "src/core/trpc/router.ts" [label= tooltip="router.ts" URL="src/core/trpc/router.ts" fillcolor="#ddfeff"] } } } + "src/core/trpc/router.ts" -> "src/core/trpc/procedures/api-config.procedure.ts" + "src/core/trpc/router.ts" -> "src/core/trpc/procedures/docker-manager.procedure.ts" + "src/core/trpc/router.ts" -> "src/core/trpc/procedures/docker-stats.procedure.ts" + "src/core/trpc/router.ts" -> "src/core/trpc/procedures/logs.procedure.ts" + "src/core/trpc/router.ts" -> "src/core/trpc/procedures/stacks.procedure.ts" + "src/core/trpc/router.ts" -> "src/core/trpc/trpc.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" "src/core/trpc/trpc.ts" [label= tooltip="trpc.ts" URL="src/core/trpc/trpc.ts" fillcolor="#ddfeff"] } } } + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/calculations.ts" [label= tooltip="calculations.ts" URL="src/core/utils/calculations.ts" fillcolor="#ddfeff"] } } } + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/change-me-checker.ts" [label= tooltip="change-me-checker.ts" URL="src/core/utils/change-me-checker.ts" fillcolor="#ddfeff"] } } } + "src/core/utils/change-me-checker.ts" -> "src/core/utils/logger.ts" + "src/core/utils/change-me-checker.ts" -> "fs/promises" [style="dashed" penwidth="1.0"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/logger.ts" [label= tooltip="logger.ts" URL="src/core/utils/logger.ts" fillcolor="#ddfeff"] } } } + "src/core/utils/logger.ts" -> "src/core/database/repository.ts" [arrowhead="normalnoneodot"] + "src/core/utils/logger.ts" -> "path" [style="dashed" penwidth="1.0"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/package-json.ts" [label= tooltip="package-json.ts" URL="src/core/utils/package-json.ts" fillcolor="#ddfeff"] } } } + "src/core/utils/package-json.ts" -> "package.json" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/respone-handler.ts" [label= tooltip="respone-handler.ts" URL="src/core/utils/respone-handler.ts" fillcolor="#ddfeff"] } } } + "src/core/utils/respone-handler.ts" -> "src/core/utils/logger.ts" + subgraph "cluster_src" {label="src" "src/index.ts" [label= tooltip="index.ts" URL="src/index.ts" fillcolor="#ddfeff"] } + "src/index.ts" -> "src/routes/stacks.ts" + "src/index.ts" -> "src/core/database/repository.ts" + "src/index.ts" -> "src/core/docker/scheduler.ts" + "src/index.ts" -> "src/core/plugins/loader.ts" + "src/index.ts" -> "src/core/trpc/index.ts" + "src/index.ts" -> "src/core/utils/logger.ts" + "src/index.ts" -> "src/routes/api-config.ts" + "src/index.ts" -> "src/routes/docker-manager.ts" + "src/index.ts" -> "src/routes/docker-stats.ts" + "src/index.ts" -> "src/routes/docker-websocket.ts" + "src/index.ts" -> "src/routes/logs.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/api-config.ts" [label= tooltip="api-config.ts" URL="src/routes/api-config.ts" fillcolor="#ddfeff"] } } + "src/routes/api-config.ts" -> "src/core/database/repository.ts" + "src/routes/api-config.ts" -> "src/core/utils/logger.ts" + "src/routes/api-config.ts" -> "src/core/utils/package-json.ts" + "src/routes/api-config.ts" -> "src/core/utils/respone-handler.ts" + "src/routes/api-config.ts" -> "src/typings/database.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/docker-manager.ts" [label= tooltip="docker-manager.ts" URL="src/routes/docker-manager.ts" fillcolor="#ddfeff"] } } + "src/routes/docker-manager.ts" -> "src/core/database/repository.ts" + "src/routes/docker-manager.ts" -> "src/core/utils/logger.ts" + "src/routes/docker-manager.ts" -> "src/core/utils/respone-handler.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/docker-stats.ts" [label= tooltip="docker-stats.ts" URL="src/routes/docker-stats.ts" fillcolor="#ddfeff"] } } + "src/routes/docker-stats.ts" -> "src/core/database/repository.ts" + "src/routes/docker-stats.ts" -> "src/core/docker/client.ts" + "src/routes/docker-stats.ts" -> "src/core/utils/calculations.ts" + "src/routes/docker-stats.ts" -> "src/core/utils/logger.ts" + "src/routes/docker-stats.ts" -> "src/core/utils/respone-handler.ts" + "src/routes/docker-stats.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] + "src/routes/docker-stats.ts" -> "src/typings/dockerode.ts" [arrowhead="onormal" penwidth="1.0"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/docker-websocket.ts" [label= tooltip="docker-websocket.ts" URL="src/routes/docker-websocket.ts" fillcolor="#ddfeff"] } } + "src/routes/docker-websocket.ts" -> "src/core/database/repository.ts" + "src/routes/docker-websocket.ts" -> "src/core/docker/client.ts" + "src/routes/docker-websocket.ts" -> "src/core/utils/calculations.ts" + "src/routes/docker-websocket.ts" -> "src/core/utils/logger.ts" + "src/routes/docker-websocket.ts" -> "src/core/utils/respone-handler.ts" + "src/routes/docker-websocket.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] + "src/routes/docker-websocket.ts" -> "src/typings/websocket.ts" [arrowhead="onormal" penwidth="1.0"] + "src/routes/docker-websocket.ts" -> "stream" [style="dashed" penwidth="1.0" arrowhead="onormal"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/logs.ts" [label= tooltip="logs.ts" URL="src/routes/logs.ts" fillcolor="#ddfeff"] } } + "src/routes/logs.ts" -> "src/core/database/repository.ts" + "src/routes/logs.ts" -> "src/core/utils/logger.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/stacks.ts" [label= tooltip="stacks.ts" URL="src/routes/stacks.ts" fillcolor="#ddfeff"] } } + "src/routes/stacks.ts" -> "src/core/database/repository.ts" + "src/routes/stacks.ts" -> "src/core/stacks/controller.ts" + "src/routes/stacks.ts" -> "src/core/utils/logger.ts" + "src/routes/stacks.ts" -> "src/core/utils/respone-handler.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/database.ts" [label= tooltip="database.ts" URL="src/typings/database.ts" fillcolor="#ddfeff"] } } + subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/docker-compose.ts" [label= tooltip="docker-compose.ts" URL="src/typings/docker-compose.ts" fillcolor="#ddfeff"] } } + subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/docker.ts" [label= tooltip="docker.ts" URL="src/typings/docker.ts" fillcolor="#ddfeff"] } } + subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/dockerode.ts" [label= tooltip="dockerode.ts" URL="src/typings/dockerode.ts" fillcolor="#ddfeff"] } } + subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/plugin.ts" [label= tooltip="plugin.ts" URL="src/typings/plugin.ts" fillcolor="#ddfeff"] } } + "src/typings/plugin.ts" -> "src/typings/docker.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/websocket.ts" [label= tooltip="websocket.ts" URL="src/typings/websocket.ts" fillcolor="#ddfeff"] } } + "src/typings/websocket.ts" -> "stream" [style="dashed" penwidth="1.0" arrowhead="onormal"] + "stream" [label= tooltip="stream" URL="https://nodejs.org/api/stream.html" color="grey" fontcolor="grey"] +} diff --git a/dependency-graph.mmd b/dependency-graph.mmd new file mode 100644 index 0000000..90c1aa9 --- /dev/null +++ b/dependency-graph.mmd @@ -0,0 +1,186 @@ + +--- +config: + flowchart: + defaultRenderer: elk +--- + +flowchart LR + +subgraph 0["src"] +1["index.ts"] +subgraph 2["routes"] +3["stacks.ts"] +1A["api-config.ts"] +1B["docker-manager.ts"] +1C["docker-stats.ts"] +1D["docker-websocket.ts"] +1G["logs.ts"] +end +subgraph 4["core"] +subgraph 5["database"] +6["repository.ts"] +8["helper.ts"] +end +subgraph 9["utils"] +A["logger.ts"] +I["respone-handler.ts"] +P["calculations.ts"] +T["change-me-checker.ts"] +14["package-json.ts"] +end +subgraph F["stacks"] +G["controller.ts"] +end +subgraph J["docker"] +K["scheduler.ts"] +L["store-host-stats.ts"] +M["client.ts"] +O["store-container-stats.ts"] +end +subgraph Q["plugins"] +R["loader.ts"] +V["plugin-manager.ts"] +end +subgraph Y["trpc"] +Z["index.ts"] +10["router.ts"] +subgraph 11["procedures"] +12["api-config.procedure.ts"] +16["docker-manager.procedure.ts"] +17["docker-stats.procedure.ts"] +18["logs.procedure.ts"] +19["stacks.procedure.ts"] +end +13["trpc.ts"] +end +end +subgraph C["typings"] +D["database.ts"] +E["docker.ts"] +H["docker-compose.ts"] +N["dockerode.ts"] +X["plugin.ts"] +1F["websocket.ts"] +end +end +7["bun:sqlite"] +B["path"] +subgraph S["fs"] +U["promises"] +end +W["events"] +15["package.json"] +1E["stream"] +1-->3 +1-->6 +1-->K +1-->R +1-->Z +1-->A +1-->1A +1-->1B +1-->1C +1-->1D +1-->1G +3-->6 +3-->G +3-->A +3-->I +6-->8 +6-->A +6-->D +6-->E +6-->7 +8-->A +A-->6 +A-->B +G-->6 +G-->A +G-->D +G-->H +I-->A +K-->6 +K-->L +K-->O +K-->A +K-->D +L-->6 +L-->M +L-->A +L-->E +L-->N +M-->A +M-->E +O-->6 +O-->M +O-->P +R-->T +R-->A +R-->V +R-->S +R-->B +T-->A +T-->U +V-->A +V-->E +V-->X +V-->W +X-->E +Z-->10 +10-->12 +10-->16 +10-->17 +10-->18 +10-->19 +10-->13 +12-->13 +12-->6 +12-->A +12-->14 +12-->D +14-->15 +16-->13 +16-->6 +16-->A +17-->13 +17-->6 +17-->M +17-->P +17-->A +17-->E +17-->N +18-->13 +18-->6 +18-->A +19-->13 +19-->6 +19-->G +19-->A +1A-->6 +1A-->A +1A-->14 +1A-->I +1A-->D +1B-->6 +1B-->A +1B-->I +1C-->6 +1C-->M +1C-->P +1C-->A +1C-->I +1C-->E +1C-->N +1D-->6 +1D-->M +1D-->P +1D-->A +1D-->I +1D-->E +1D-->1F +1D-->1E +1F-->1E +1G-->6 +1G-->A + diff --git a/dependency-graph.svg b/dependency-graph.svg new file mode 100644 index 0000000..5dd8bde --- /dev/null +++ b/dependency-graph.svg @@ -0,0 +1,1125 @@ + + + + + + +dependency-cruiser output + + +cluster_fs + +fs + + +cluster_src + +src + + +cluster_src/core + +core + + +cluster_src/core/database + +database + + +cluster_src/core/docker + +docker + + +cluster_src/core/plugins + +plugins + + +cluster_src/core/stacks + +stacks + + +cluster_src/core/trpc + +trpc + + +cluster_src/core/trpc/procedures + +procedures + + +cluster_src/core/utils + +utils + + +cluster_src/routes + +routes + + +cluster_src/typings + +typings + + + +bun:sqlite + + +bun:sqlite + + + + + +events + + +events + + + + + +fs + + +fs + + + + + +fs/promises + + +promises + + + + + +package.json + + +package.json + + + + + +path + + +path + + + + + +src/core/database/helper.ts + + +helper.ts + + + + + +src/core/utils/logger.ts + + +logger.ts + + + + + +src/core/database/helper.ts->src/core/utils/logger.ts + + + + + + + +src/core/utils/logger.ts->path + + + + + +src/core/database/repository.ts + + +repository.ts + + + + + +src/core/utils/logger.ts->src/core/database/repository.ts + + + + + + + +src/core/database/repository.ts->bun:sqlite + + + + + +src/core/database/repository.ts->src/core/database/helper.ts + + + + + + + +src/core/database/repository.ts->src/core/utils/logger.ts + + + + + + + +src/typings/database.ts + + +database.ts + + + + + +src/core/database/repository.ts->src/typings/database.ts + + + + + +src/typings/docker.ts + + +docker.ts + + + + + +src/core/database/repository.ts->src/typings/docker.ts + + + + + +src/core/docker/client.ts + + +client.ts + + + + + +src/core/docker/client.ts->src/core/utils/logger.ts + + + + + +src/core/docker/client.ts->src/typings/docker.ts + + + + + +src/core/docker/scheduler.ts + + +scheduler.ts + + + + + +src/core/docker/scheduler.ts->src/core/utils/logger.ts + + + + + +src/core/docker/scheduler.ts->src/core/database/repository.ts + + + + + +src/core/docker/scheduler.ts->src/typings/database.ts + + + + + +src/core/docker/store-host-stats.ts + + +store-host-stats.ts + + + + + +src/core/docker/scheduler.ts->src/core/docker/store-host-stats.ts + + + + + +src/core/docker/store-container-stats.ts + + +store-container-stats.ts + + + + + +src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts + + + + + +src/core/docker/store-host-stats.ts->src/core/utils/logger.ts + + + + + +src/core/docker/store-host-stats.ts->src/core/database/repository.ts + + + + + +src/core/docker/store-host-stats.ts->src/typings/docker.ts + + + + + +src/core/docker/store-host-stats.ts->src/core/docker/client.ts + + + + + +src/typings/dockerode.ts + + +dockerode.ts + + + + + +src/core/docker/store-host-stats.ts->src/typings/dockerode.ts + + + + + +src/core/docker/store-container-stats.ts->src/core/database/repository.ts + + + + + +src/core/docker/store-container-stats.ts->src/core/docker/client.ts + + + + + +src/core/utils/calculations.ts + + +calculations.ts + + + + + +src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts + + + + + +src/core/plugins/loader.ts + + +loader.ts + + + + + +src/core/plugins/loader.ts->fs + + + + + +src/core/plugins/loader.ts->path + + + + + +src/core/plugins/loader.ts->src/core/utils/logger.ts + + + + + +src/core/utils/change-me-checker.ts + + +change-me-checker.ts + + + + + +src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts + + + + + +src/core/plugins/plugin-manager.ts + + +plugin-manager.ts + + + + + +src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts + + + + + +src/core/utils/change-me-checker.ts->fs/promises + + + + + +src/core/utils/change-me-checker.ts->src/core/utils/logger.ts + + + + + +src/core/plugins/plugin-manager.ts->events + + + + + +src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts + + + + + +src/core/plugins/plugin-manager.ts->src/typings/docker.ts + + + + + +src/typings/plugin.ts + + +plugin.ts + + + + + +src/core/plugins/plugin-manager.ts->src/typings/plugin.ts + + + + + +src/typings/plugin.ts->src/typings/docker.ts + + + + + +src/core/stacks/controller.ts + + +controller.ts + + + + + +src/core/stacks/controller.ts->src/core/utils/logger.ts + + + + + +src/core/stacks/controller.ts->src/core/database/repository.ts + + + + + +src/core/stacks/controller.ts->src/typings/database.ts + + + + + +src/typings/docker-compose.ts + + +docker-compose.ts + + + + + +src/core/stacks/controller.ts->src/typings/docker-compose.ts + + + + + +src/core/trpc/index.ts + + +index.ts + + + + + +src/core/trpc/router.ts + + +router.ts + + + + + +src/core/trpc/index.ts->src/core/trpc/router.ts + + + + + +src/core/trpc/procedures/api-config.procedure.ts + + +api-config.procedure.ts + + + + + +src/core/trpc/router.ts->src/core/trpc/procedures/api-config.procedure.ts + + + + + +src/core/trpc/trpc.ts + + +trpc.ts + + + + + +src/core/trpc/router.ts->src/core/trpc/trpc.ts + + + + + +src/core/trpc/procedures/docker-manager.procedure.ts + + +docker-manager.procedure.ts + + + + + +src/core/trpc/router.ts->src/core/trpc/procedures/docker-manager.procedure.ts + + + + + +src/core/trpc/procedures/docker-stats.procedure.ts + + +docker-stats.procedure.ts + + + + + +src/core/trpc/router.ts->src/core/trpc/procedures/docker-stats.procedure.ts + + + + + +src/core/trpc/procedures/logs.procedure.ts + + +logs.procedure.ts + + + + + +src/core/trpc/router.ts->src/core/trpc/procedures/logs.procedure.ts + + + + + +src/core/trpc/procedures/stacks.procedure.ts + + +stacks.procedure.ts + + + + + +src/core/trpc/router.ts->src/core/trpc/procedures/stacks.procedure.ts + + + + + +src/core/trpc/procedures/api-config.procedure.ts->src/core/utils/logger.ts + + + + + +src/core/trpc/procedures/api-config.procedure.ts->src/core/database/repository.ts + + + + + +src/core/trpc/procedures/api-config.procedure.ts->src/typings/database.ts + + + + + +src/core/trpc/procedures/api-config.procedure.ts->src/core/trpc/trpc.ts + + + + + +src/core/utils/package-json.ts + + +package-json.ts + + + + + +src/core/trpc/procedures/api-config.procedure.ts->src/core/utils/package-json.ts + + + + + +src/core/utils/package-json.ts->package.json + + + + + +src/core/trpc/procedures/docker-manager.procedure.ts->src/core/utils/logger.ts + + + + + +src/core/trpc/procedures/docker-manager.procedure.ts->src/core/database/repository.ts + + + + + +src/core/trpc/procedures/docker-manager.procedure.ts->src/core/trpc/trpc.ts + + + + + +src/core/trpc/procedures/docker-stats.procedure.ts->src/core/utils/logger.ts + + + + + +src/core/trpc/procedures/docker-stats.procedure.ts->src/core/database/repository.ts + + + + + +src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/docker.ts + + + + + +src/core/trpc/procedures/docker-stats.procedure.ts->src/core/docker/client.ts + + + + + +src/core/trpc/procedures/docker-stats.procedure.ts->src/core/utils/calculations.ts + + + + + +src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/dockerode.ts + + + + + +src/core/trpc/procedures/docker-stats.procedure.ts->src/core/trpc/trpc.ts + + + + + +src/core/trpc/procedures/logs.procedure.ts->src/core/utils/logger.ts + + + + + +src/core/trpc/procedures/logs.procedure.ts->src/core/database/repository.ts + + + + + +src/core/trpc/procedures/logs.procedure.ts->src/core/trpc/trpc.ts + + + + + +src/core/trpc/procedures/stacks.procedure.ts->src/core/utils/logger.ts + + + + + +src/core/trpc/procedures/stacks.procedure.ts->src/core/database/repository.ts + + + + + +src/core/trpc/procedures/stacks.procedure.ts->src/core/stacks/controller.ts + + + + + +src/core/trpc/procedures/stacks.procedure.ts->src/core/trpc/trpc.ts + + + + + +src/core/utils/respone-handler.ts + + +respone-handler.ts + + + + + +src/core/utils/respone-handler.ts->src/core/utils/logger.ts + + + + + +src/index.ts + + +index.ts + + + + + +src/index.ts->src/core/utils/logger.ts + + + + + +src/index.ts->src/core/database/repository.ts + + + + + +src/index.ts->src/core/docker/scheduler.ts + + + + + +src/index.ts->src/core/plugins/loader.ts + + + + + +src/index.ts->src/core/trpc/index.ts + + + + + +src/routes/stacks.ts + + +stacks.ts + + + + + +src/index.ts->src/routes/stacks.ts + + + + + +src/routes/api-config.ts + + +api-config.ts + + + + + +src/index.ts->src/routes/api-config.ts + + + + + +src/routes/docker-manager.ts + + +docker-manager.ts + + + + + +src/index.ts->src/routes/docker-manager.ts + + + + + +src/routes/docker-stats.ts + + +docker-stats.ts + + + + + +src/index.ts->src/routes/docker-stats.ts + + + + + +src/routes/docker-websocket.ts + + +docker-websocket.ts + + + + + +src/index.ts->src/routes/docker-websocket.ts + + + + + +src/routes/logs.ts + + +logs.ts + + + + + +src/index.ts->src/routes/logs.ts + + + + + +src/routes/stacks.ts->src/core/utils/logger.ts + + + + + +src/routes/stacks.ts->src/core/database/repository.ts + + + + + +src/routes/stacks.ts->src/core/stacks/controller.ts + + + + + +src/routes/stacks.ts->src/core/utils/respone-handler.ts + + + + + +src/routes/api-config.ts->src/core/utils/logger.ts + + + + + +src/routes/api-config.ts->src/core/database/repository.ts + + + + + +src/routes/api-config.ts->src/typings/database.ts + + + + + +src/routes/api-config.ts->src/core/utils/package-json.ts + + + + + +src/routes/api-config.ts->src/core/utils/respone-handler.ts + + + + + +src/routes/docker-manager.ts->src/core/utils/logger.ts + + + + + +src/routes/docker-manager.ts->src/core/database/repository.ts + + + + + +src/routes/docker-manager.ts->src/core/utils/respone-handler.ts + + + + + +src/routes/docker-stats.ts->src/core/utils/logger.ts + + + + + +src/routes/docker-stats.ts->src/core/database/repository.ts + + + + + +src/routes/docker-stats.ts->src/typings/docker.ts + + + + + +src/routes/docker-stats.ts->src/core/docker/client.ts + + + + + +src/routes/docker-stats.ts->src/core/utils/calculations.ts + + + + + +src/routes/docker-stats.ts->src/typings/dockerode.ts + + + + + +src/routes/docker-stats.ts->src/core/utils/respone-handler.ts + + + + + +src/routes/docker-websocket.ts->src/core/utils/logger.ts + + + + + +src/routes/docker-websocket.ts->src/core/database/repository.ts + + + + + +src/routes/docker-websocket.ts->src/typings/docker.ts + + + + + +src/routes/docker-websocket.ts->src/core/docker/client.ts + + + + + +src/routes/docker-websocket.ts->src/core/utils/calculations.ts + + + + + +src/routes/docker-websocket.ts->src/core/utils/respone-handler.ts + + + + + +src/typings/websocket.ts + + +websocket.ts + + + + + +src/routes/docker-websocket.ts->src/typings/websocket.ts + + + + + +stream + + +stream + + + + + +src/routes/docker-websocket.ts->stream + + + + + +src/routes/logs.ts->src/core/utils/logger.ts + + + + + +src/routes/logs.ts->src/core/database/repository.ts + + + + + +src/typings/websocket.ts->stream + + + + + From 47a277e4ad73a23fbed4fb50adde57215567df8a Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 14 Mar 2025 00:10:59 +0100 Subject: [PATCH 183/324] Fix: Switch to DOTT --- .github/scripts/dep-graph.sh | 3 +-- .github/workflows/dependency-graph.yml | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/scripts/dep-graph.sh b/.github/scripts/dep-graph.sh index d702dc1..be7d873 100644 --- a/.github/scripts/dep-graph.sh +++ b/.github/scripts/dep-graph.sh @@ -2,8 +2,7 @@ mermaidContent="$(cat dependency-graph.mmd)" -echo " ---- +echo "--- config: flowchart: defaultRenderer: elk diff --git a/.github/workflows/dependency-graph.yml b/.github/workflows/dependency-graph.yml index 019e039..9592a45 100644 --- a/.github/workflows/dependency-graph.yml +++ b/.github/workflows/dependency-graph.yml @@ -31,7 +31,7 @@ jobs: - name: Generate Dependency Graph (SVG) run: | - bun run dependency-cruiser --output-type dot src/index.ts --output-to dependency-graph.dot --no-config -x node_modules --ts-pre-compilation-deps --ts-config tsconfig.json + bun run dependency-cruiser --output-type dott src/index.ts --output-to dependency-graph.dot --no-config -x node_modules --ts-pre-compilation-deps --ts-config tsconfig.json dot -Tsvg dependency-graph.dot -o dependency-graph.svg echo "SVG graph generated at dependency-graph.svg" From 89a6d953484ae5e53207b37dbdcbd5a23d4c20fd Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 14 Mar 2025 00:12:10 +0100 Subject: [PATCH 184/324] Fix: I meant archi --- .github/workflows/dependency-graph.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependency-graph.yml b/.github/workflows/dependency-graph.yml index 9592a45..7732876 100644 --- a/.github/workflows/dependency-graph.yml +++ b/.github/workflows/dependency-graph.yml @@ -31,7 +31,7 @@ jobs: - name: Generate Dependency Graph (SVG) run: | - bun run dependency-cruiser --output-type dott src/index.ts --output-to dependency-graph.dot --no-config -x node_modules --ts-pre-compilation-deps --ts-config tsconfig.json + bun run dependency-cruiser --output-type archi src/index.ts --output-to dependency-graph.dot --no-config -x node_modules --ts-pre-compilation-deps --ts-config tsconfig.json dot -Tsvg dependency-graph.dot -o dependency-graph.svg echo "SVG graph generated at dependency-graph.svg" From 886098c13ac1af5a15faee7f37a9ae81d26511ca Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Thu, 13 Mar 2025 23:12:37 +0000 Subject: [PATCH 185/324] Update dependency graphs --- dependency-graph.dot | 161 +----- dependency-graph.mmd | 1 - dependency-graph.svg | 1150 +++++------------------------------------- 3 files changed, 133 insertions(+), 1179 deletions(-) diff --git a/dependency-graph.dot b/dependency-graph.dot index 4cf81c6..277a1eb 100644 --- a/dependency-graph.dot +++ b/dependency-graph.dot @@ -9,151 +9,22 @@ strict digraph "dependency-cruiser output"{ subgraph "cluster_fs" {label="fs" "fs/promises" [label= tooltip="promises" URL="https://nodejs.org/api/fs.html" color="grey" fontcolor="grey"] } "package.json" [label= tooltip="package.json" URL="package.json" fillcolor="#ffee44"] "path" [label= tooltip="path" URL="https://nodejs.org/api/path.html" color="grey" fontcolor="grey"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/helper.ts" [label= tooltip="helper.ts" URL="src/core/database/helper.ts" fillcolor="#ddfeff"] } } } - "src/core/database/helper.ts" -> "src/core/utils/logger.ts" [arrowhead="normalnoneodot"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/repository.ts" [label= tooltip="repository.ts" URL="src/core/database/repository.ts" fillcolor="#ddfeff"] } } } - "src/core/database/repository.ts" -> "src/core/database/helper.ts" [arrowhead="normalnoneodot"] - "src/core/database/repository.ts" -> "src/core/utils/logger.ts" [arrowhead="normalnoneodot"] - "src/core/database/repository.ts" -> "src/typings/database.ts" [arrowhead="onormal" penwidth="1.0"] - "src/core/database/repository.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] - "src/core/database/repository.ts" -> "bun:sqlite" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/client.ts" [label= tooltip="client.ts" URL="src/core/docker/client.ts" fillcolor="#ddfeff"] } } } - "src/core/docker/client.ts" -> "src/core/utils/logger.ts" - "src/core/docker/client.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/scheduler.ts" [label= tooltip="scheduler.ts" URL="src/core/docker/scheduler.ts" fillcolor="#ddfeff"] } } } - "src/core/docker/scheduler.ts" -> "src/core/database/repository.ts" - "src/core/docker/scheduler.ts" -> "src/core/docker/store-host-stats.ts" - "src/core/docker/scheduler.ts" -> "src/core/docker/store-container-stats.ts" - "src/core/docker/scheduler.ts" -> "src/core/utils/logger.ts" - "src/core/docker/scheduler.ts" -> "src/typings/database.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/store-container-stats.ts" [label= tooltip="store-container-stats.ts" URL="src/core/docker/store-container-stats.ts" fillcolor="#ddfeff"] } } } - "src/core/docker/store-container-stats.ts" -> "src/core/database/repository.ts" - "src/core/docker/store-container-stats.ts" -> "src/core/docker/client.ts" - "src/core/docker/store-container-stats.ts" -> "src/core/utils/calculations.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/store-host-stats.ts" [label= tooltip="store-host-stats.ts" URL="src/core/docker/store-host-stats.ts" fillcolor="#ddfeff"] } } } - "src/core/docker/store-host-stats.ts" -> "src/core/database/repository.ts" - "src/core/docker/store-host-stats.ts" -> "src/core/docker/client.ts" - "src/core/docker/store-host-stats.ts" -> "src/core/utils/logger.ts" - "src/core/docker/store-host-stats.ts" -> "src/typings/docker.ts" - "src/core/docker/store-host-stats.ts" -> "src/typings/dockerode.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/plugins" {label="plugins" "src/core/plugins/loader.ts" [label= tooltip="loader.ts" URL="src/core/plugins/loader.ts" fillcolor="#ddfeff"] } } } - "src/core/plugins/loader.ts" -> "src/core/utils/change-me-checker.ts" - "src/core/plugins/loader.ts" -> "src/core/utils/logger.ts" - "src/core/plugins/loader.ts" -> "src/core/plugins/plugin-manager.ts" - "src/core/plugins/loader.ts" -> "fs" [style="dashed" penwidth="1.0"] - "src/core/plugins/loader.ts" -> "path" [style="dashed" penwidth="1.0"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/plugins" {label="plugins" "src/core/plugins/plugin-manager.ts" [label= tooltip="plugin-manager.ts" URL="src/core/plugins/plugin-manager.ts" fillcolor="#ddfeff"] } } } - "src/core/plugins/plugin-manager.ts" -> "src/core/utils/logger.ts" - "src/core/plugins/plugin-manager.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] - "src/core/plugins/plugin-manager.ts" -> "src/typings/plugin.ts" [arrowhead="onormal" penwidth="1.0"] - "src/core/plugins/plugin-manager.ts" -> "events" [style="dashed" penwidth="1.0"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/stacks" {label="stacks" "src/core/stacks/controller.ts" [label= tooltip="controller.ts" URL="src/core/stacks/controller.ts" fillcolor="#ddfeff"] } } } - "src/core/stacks/controller.ts" -> "src/core/database/repository.ts" - "src/core/stacks/controller.ts" -> "src/core/utils/logger.ts" - "src/core/stacks/controller.ts" -> "src/typings/database.ts" [arrowhead="onormal" penwidth="1.0"] - "src/core/stacks/controller.ts" -> "src/typings/docker-compose.ts" [arrowhead="onormal" penwidth="1.0"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" "src/core/trpc/index.ts" [label= tooltip="index.ts" URL="src/core/trpc/index.ts" fillcolor="#ddfeff"] } } } - "src/core/trpc/index.ts" -> "src/core/trpc/router.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/api-config.procedure.ts" [label= tooltip="api-config.procedure.ts" URL="src/core/trpc/procedures/api-config.procedure.ts" fillcolor="#ddfeff"] } } } } - "src/core/trpc/procedures/api-config.procedure.ts" -> "src/core/trpc/trpc.ts" - "src/core/trpc/procedures/api-config.procedure.ts" -> "src/core/database/repository.ts" - "src/core/trpc/procedures/api-config.procedure.ts" -> "src/core/utils/logger.ts" - "src/core/trpc/procedures/api-config.procedure.ts" -> "src/core/utils/package-json.ts" - "src/core/trpc/procedures/api-config.procedure.ts" -> "src/typings/database.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/docker-manager.procedure.ts" [label= tooltip="docker-manager.procedure.ts" URL="src/core/trpc/procedures/docker-manager.procedure.ts" fillcolor="#ddfeff"] } } } } - "src/core/trpc/procedures/docker-manager.procedure.ts" -> "src/core/trpc/trpc.ts" - "src/core/trpc/procedures/docker-manager.procedure.ts" -> "src/core/database/repository.ts" - "src/core/trpc/procedures/docker-manager.procedure.ts" -> "src/core/utils/logger.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/docker-stats.procedure.ts" [label= tooltip="docker-stats.procedure.ts" URL="src/core/trpc/procedures/docker-stats.procedure.ts" fillcolor="#ddfeff"] } } } } - "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/trpc/trpc.ts" - "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/database/repository.ts" - "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/docker/client.ts" - "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/utils/calculations.ts" - "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/utils/logger.ts" - "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] - "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/typings/dockerode.ts" [arrowhead="onormal" penwidth="1.0"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/logs.procedure.ts" [label= tooltip="logs.procedure.ts" URL="src/core/trpc/procedures/logs.procedure.ts" fillcolor="#ddfeff"] } } } } - "src/core/trpc/procedures/logs.procedure.ts" -> "src/core/trpc/trpc.ts" - "src/core/trpc/procedures/logs.procedure.ts" -> "src/core/database/repository.ts" - "src/core/trpc/procedures/logs.procedure.ts" -> "src/core/utils/logger.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/stacks.procedure.ts" [label= tooltip="stacks.procedure.ts" URL="src/core/trpc/procedures/stacks.procedure.ts" fillcolor="#ddfeff"] } } } } - "src/core/trpc/procedures/stacks.procedure.ts" -> "src/core/trpc/trpc.ts" - "src/core/trpc/procedures/stacks.procedure.ts" -> "src/core/database/repository.ts" - "src/core/trpc/procedures/stacks.procedure.ts" -> "src/core/stacks/controller.ts" - "src/core/trpc/procedures/stacks.procedure.ts" -> "src/core/utils/logger.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" "src/core/trpc/router.ts" [label= tooltip="router.ts" URL="src/core/trpc/router.ts" fillcolor="#ddfeff"] } } } - "src/core/trpc/router.ts" -> "src/core/trpc/procedures/api-config.procedure.ts" - "src/core/trpc/router.ts" -> "src/core/trpc/procedures/docker-manager.procedure.ts" - "src/core/trpc/router.ts" -> "src/core/trpc/procedures/docker-stats.procedure.ts" - "src/core/trpc/router.ts" -> "src/core/trpc/procedures/logs.procedure.ts" - "src/core/trpc/router.ts" -> "src/core/trpc/procedures/stacks.procedure.ts" - "src/core/trpc/router.ts" -> "src/core/trpc/trpc.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" "src/core/trpc/trpc.ts" [label= tooltip="trpc.ts" URL="src/core/trpc/trpc.ts" fillcolor="#ddfeff"] } } } - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/calculations.ts" [label= tooltip="calculations.ts" URL="src/core/utils/calculations.ts" fillcolor="#ddfeff"] } } } - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/change-me-checker.ts" [label= tooltip="change-me-checker.ts" URL="src/core/utils/change-me-checker.ts" fillcolor="#ddfeff"] } } } - "src/core/utils/change-me-checker.ts" -> "src/core/utils/logger.ts" - "src/core/utils/change-me-checker.ts" -> "fs/promises" [style="dashed" penwidth="1.0"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/logger.ts" [label= tooltip="logger.ts" URL="src/core/utils/logger.ts" fillcolor="#ddfeff"] } } } - "src/core/utils/logger.ts" -> "src/core/database/repository.ts" [arrowhead="normalnoneodot"] - "src/core/utils/logger.ts" -> "path" [style="dashed" penwidth="1.0"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/package-json.ts" [label= tooltip="package-json.ts" URL="src/core/utils/package-json.ts" fillcolor="#ddfeff"] } } } - "src/core/utils/package-json.ts" -> "package.json" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/respone-handler.ts" [label= tooltip="respone-handler.ts" URL="src/core/utils/respone-handler.ts" fillcolor="#ddfeff"] } } } - "src/core/utils/respone-handler.ts" -> "src/core/utils/logger.ts" + subgraph "cluster_src" {label="src" "src/core" [label= tooltip="core" URL="src/core" shape="box3d"] } + "src/core" -> "src/typings" [arrowhead="onormal" penwidth="1.0"] + "src/core" -> "bun:sqlite" + "src/core" -> "path" [style="dashed" penwidth="1.0"] + "src/core" -> "fs" [style="dashed" penwidth="1.0"] + "src/core" -> "fs/promises" [style="dashed" penwidth="1.0"] + "src/core" -> "events" [style="dashed" penwidth="1.0"] + "src/core" -> "package.json" subgraph "cluster_src" {label="src" "src/index.ts" [label= tooltip="index.ts" URL="src/index.ts" fillcolor="#ddfeff"] } - "src/index.ts" -> "src/routes/stacks.ts" - "src/index.ts" -> "src/core/database/repository.ts" - "src/index.ts" -> "src/core/docker/scheduler.ts" - "src/index.ts" -> "src/core/plugins/loader.ts" - "src/index.ts" -> "src/core/trpc/index.ts" - "src/index.ts" -> "src/core/utils/logger.ts" - "src/index.ts" -> "src/routes/api-config.ts" - "src/index.ts" -> "src/routes/docker-manager.ts" - "src/index.ts" -> "src/routes/docker-stats.ts" - "src/index.ts" -> "src/routes/docker-websocket.ts" - "src/index.ts" -> "src/routes/logs.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/api-config.ts" [label= tooltip="api-config.ts" URL="src/routes/api-config.ts" fillcolor="#ddfeff"] } } - "src/routes/api-config.ts" -> "src/core/database/repository.ts" - "src/routes/api-config.ts" -> "src/core/utils/logger.ts" - "src/routes/api-config.ts" -> "src/core/utils/package-json.ts" - "src/routes/api-config.ts" -> "src/core/utils/respone-handler.ts" - "src/routes/api-config.ts" -> "src/typings/database.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/docker-manager.ts" [label= tooltip="docker-manager.ts" URL="src/routes/docker-manager.ts" fillcolor="#ddfeff"] } } - "src/routes/docker-manager.ts" -> "src/core/database/repository.ts" - "src/routes/docker-manager.ts" -> "src/core/utils/logger.ts" - "src/routes/docker-manager.ts" -> "src/core/utils/respone-handler.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/docker-stats.ts" [label= tooltip="docker-stats.ts" URL="src/routes/docker-stats.ts" fillcolor="#ddfeff"] } } - "src/routes/docker-stats.ts" -> "src/core/database/repository.ts" - "src/routes/docker-stats.ts" -> "src/core/docker/client.ts" - "src/routes/docker-stats.ts" -> "src/core/utils/calculations.ts" - "src/routes/docker-stats.ts" -> "src/core/utils/logger.ts" - "src/routes/docker-stats.ts" -> "src/core/utils/respone-handler.ts" - "src/routes/docker-stats.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] - "src/routes/docker-stats.ts" -> "src/typings/dockerode.ts" [arrowhead="onormal" penwidth="1.0"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/docker-websocket.ts" [label= tooltip="docker-websocket.ts" URL="src/routes/docker-websocket.ts" fillcolor="#ddfeff"] } } - "src/routes/docker-websocket.ts" -> "src/core/database/repository.ts" - "src/routes/docker-websocket.ts" -> "src/core/docker/client.ts" - "src/routes/docker-websocket.ts" -> "src/core/utils/calculations.ts" - "src/routes/docker-websocket.ts" -> "src/core/utils/logger.ts" - "src/routes/docker-websocket.ts" -> "src/core/utils/respone-handler.ts" - "src/routes/docker-websocket.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] - "src/routes/docker-websocket.ts" -> "src/typings/websocket.ts" [arrowhead="onormal" penwidth="1.0"] - "src/routes/docker-websocket.ts" -> "stream" [style="dashed" penwidth="1.0" arrowhead="onormal"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/logs.ts" [label= tooltip="logs.ts" URL="src/routes/logs.ts" fillcolor="#ddfeff"] } } - "src/routes/logs.ts" -> "src/core/database/repository.ts" - "src/routes/logs.ts" -> "src/core/utils/logger.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/stacks.ts" [label= tooltip="stacks.ts" URL="src/routes/stacks.ts" fillcolor="#ddfeff"] } } - "src/routes/stacks.ts" -> "src/core/database/repository.ts" - "src/routes/stacks.ts" -> "src/core/stacks/controller.ts" - "src/routes/stacks.ts" -> "src/core/utils/logger.ts" - "src/routes/stacks.ts" -> "src/core/utils/respone-handler.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/database.ts" [label= tooltip="database.ts" URL="src/typings/database.ts" fillcolor="#ddfeff"] } } - subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/docker-compose.ts" [label= tooltip="docker-compose.ts" URL="src/typings/docker-compose.ts" fillcolor="#ddfeff"] } } - subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/docker.ts" [label= tooltip="docker.ts" URL="src/typings/docker.ts" fillcolor="#ddfeff"] } } - subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/dockerode.ts" [label= tooltip="dockerode.ts" URL="src/typings/dockerode.ts" fillcolor="#ddfeff"] } } - subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/plugin.ts" [label= tooltip="plugin.ts" URL="src/typings/plugin.ts" fillcolor="#ddfeff"] } } - "src/typings/plugin.ts" -> "src/typings/docker.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/websocket.ts" [label= tooltip="websocket.ts" URL="src/typings/websocket.ts" fillcolor="#ddfeff"] } } - "src/typings/websocket.ts" -> "stream" [style="dashed" penwidth="1.0" arrowhead="onormal"] + "src/index.ts" -> "src/routes" + "src/index.ts" -> "src/core" + subgraph "cluster_src" {label="src" "src/routes" [label= tooltip="routes" URL="src/routes" shape="box3d"] } + "src/routes" -> "src/core" + "src/routes" -> "src/typings" + "src/routes" -> "stream" [style="dashed" penwidth="1.0" arrowhead="onormal"] + subgraph "cluster_src" {label="src" "src/typings" [label= tooltip="typings" URL="src/typings" shape="box3d"] } + "src/typings" -> "stream" [style="dashed" penwidth="1.0" arrowhead="onormal"] "stream" [label= tooltip="stream" URL="https://nodejs.org/api/stream.html" color="grey" fontcolor="grey"] } diff --git a/dependency-graph.mmd b/dependency-graph.mmd index 90c1aa9..fe385f8 100644 --- a/dependency-graph.mmd +++ b/dependency-graph.mmd @@ -1,4 +1,3 @@ - --- config: flowchart: diff --git a/dependency-graph.svg b/dependency-graph.svg index 5dd8bde..c833eb6 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -4,77 +4,27 @@ - - + + dependency-cruiser output - + cluster_fs - -fs + +fs cluster_src - -src - - -cluster_src/core - -core - - -cluster_src/core/database - -database - - -cluster_src/core/docker - -docker - - -cluster_src/core/plugins - -plugins - - -cluster_src/core/stacks - -stacks - - -cluster_src/core/trpc - -trpc - - -cluster_src/core/trpc/procedures - -procedures - - -cluster_src/core/utils - -utils - - -cluster_src/routes - -routes - - -cluster_src/typings - -typings + +src bun:sqlite - -bun:sqlite + +bun:sqlite @@ -82,8 +32,8 @@ events - -events + +events @@ -91,8 +41,8 @@ fs - -fs + +fs @@ -100,8 +50,8 @@ fs/promises - -promises + +promises @@ -109,8 +59,8 @@ package.json - -package.json + +package.json @@ -118,1008 +68,142 @@ path - -path + +path - + -src/core/database/helper.ts - - -helper.ts +src/core + + + + + +core - - -src/core/utils/logger.ts - - -logger.ts - - - - - -src/core/database/helper.ts->src/core/utils/logger.ts - - - - - - - -src/core/utils/logger.ts->path - - - - - -src/core/database/repository.ts - - -repository.ts - - - - - -src/core/utils/logger.ts->src/core/database/repository.ts - - - - - - - -src/core/database/repository.ts->bun:sqlite - - - - + -src/core/database/repository.ts->src/core/database/helper.ts - - - - - - - -src/core/database/repository.ts->src/core/utils/logger.ts - - - - - - - -src/typings/database.ts - - -database.ts - +src/core->bun:sqlite + + + + +src/core->events + + - + -src/core/database/repository.ts->src/typings/database.ts - - - - - -src/typings/docker.ts - - -docker.ts - +src/core->fs + + - - + -src/core/database/repository.ts->src/typings/docker.ts - - - - - -src/core/docker/client.ts - - -client.ts - - +src/core->fs/promises + + - + -src/core/docker/client.ts->src/core/utils/logger.ts - - - - - -src/core/docker/client.ts->src/typings/docker.ts - - - - - -src/core/docker/scheduler.ts - - -scheduler.ts - - - - - -src/core/docker/scheduler.ts->src/core/utils/logger.ts - - - - - -src/core/docker/scheduler.ts->src/core/database/repository.ts - - - - - -src/core/docker/scheduler.ts->src/typings/database.ts - - - - - -src/core/docker/store-host-stats.ts - - -store-host-stats.ts - - - - - -src/core/docker/scheduler.ts->src/core/docker/store-host-stats.ts - - - - - -src/core/docker/store-container-stats.ts - - -store-container-stats.ts - - - - - -src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts - - - - - -src/core/docker/store-host-stats.ts->src/core/utils/logger.ts - - - - - -src/core/docker/store-host-stats.ts->src/core/database/repository.ts - - - - - -src/core/docker/store-host-stats.ts->src/typings/docker.ts - - - - - -src/core/docker/store-host-stats.ts->src/core/docker/client.ts - - - - - -src/typings/dockerode.ts - - -dockerode.ts - - - - - -src/core/docker/store-host-stats.ts->src/typings/dockerode.ts - - - - - -src/core/docker/store-container-stats.ts->src/core/database/repository.ts - - - - - -src/core/docker/store-container-stats.ts->src/core/docker/client.ts - - - - - -src/core/utils/calculations.ts - - -calculations.ts - - - - - -src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts - - - - - -src/core/plugins/loader.ts - - -loader.ts - - - - - -src/core/plugins/loader.ts->fs - - - - - -src/core/plugins/loader.ts->path - - - - - -src/core/plugins/loader.ts->src/core/utils/logger.ts - - - - - -src/core/utils/change-me-checker.ts - - -change-me-checker.ts - - - - - -src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts - - - - - -src/core/plugins/plugin-manager.ts - - -plugin-manager.ts - - - - - -src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts - - - - - -src/core/utils/change-me-checker.ts->fs/promises - - - - - -src/core/utils/change-me-checker.ts->src/core/utils/logger.ts - - - - - -src/core/plugins/plugin-manager.ts->events - - - - - -src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts - - - - - -src/core/plugins/plugin-manager.ts->src/typings/docker.ts - - - - - -src/typings/plugin.ts - - -plugin.ts - - - - - -src/core/plugins/plugin-manager.ts->src/typings/plugin.ts - - - - - -src/typings/plugin.ts->src/typings/docker.ts - - - - - -src/core/stacks/controller.ts - - -controller.ts - - - - - -src/core/stacks/controller.ts->src/core/utils/logger.ts - - - - - -src/core/stacks/controller.ts->src/core/database/repository.ts - - - - - -src/core/stacks/controller.ts->src/typings/database.ts - - - - - -src/typings/docker-compose.ts - - -docker-compose.ts - - - - - -src/core/stacks/controller.ts->src/typings/docker-compose.ts - - - - - -src/core/trpc/index.ts - - -index.ts - - - - - -src/core/trpc/router.ts - - -router.ts - - - - - -src/core/trpc/index.ts->src/core/trpc/router.ts - - - - - -src/core/trpc/procedures/api-config.procedure.ts - - -api-config.procedure.ts - - +src/core->package.json + + - - -src/core/trpc/router.ts->src/core/trpc/procedures/api-config.procedure.ts - - - - - -src/core/trpc/trpc.ts - - -trpc.ts - - - - - -src/core/trpc/router.ts->src/core/trpc/trpc.ts - - - - - -src/core/trpc/procedures/docker-manager.procedure.ts - - -docker-manager.procedure.ts - - - - - -src/core/trpc/router.ts->src/core/trpc/procedures/docker-manager.procedure.ts - - - - - -src/core/trpc/procedures/docker-stats.procedure.ts - - -docker-stats.procedure.ts - - - - - -src/core/trpc/router.ts->src/core/trpc/procedures/docker-stats.procedure.ts - - - - - -src/core/trpc/procedures/logs.procedure.ts - - -logs.procedure.ts - - - - - -src/core/trpc/router.ts->src/core/trpc/procedures/logs.procedure.ts - - - - - -src/core/trpc/procedures/stacks.procedure.ts - - -stacks.procedure.ts - - - - - -src/core/trpc/router.ts->src/core/trpc/procedures/stacks.procedure.ts - - - - - -src/core/trpc/procedures/api-config.procedure.ts->src/core/utils/logger.ts - - - - - -src/core/trpc/procedures/api-config.procedure.ts->src/core/database/repository.ts - - - - - -src/core/trpc/procedures/api-config.procedure.ts->src/typings/database.ts - - - - - -src/core/trpc/procedures/api-config.procedure.ts->src/core/trpc/trpc.ts - - + + +src/core->path + + - - -src/core/utils/package-json.ts - - -package-json.ts + + +src/typings + + + + + +typings - - -src/core/trpc/procedures/api-config.procedure.ts->src/core/utils/package-json.ts - - - - - -src/core/utils/package-json.ts->package.json - - - - - -src/core/trpc/procedures/docker-manager.procedure.ts->src/core/utils/logger.ts - - - - - -src/core/trpc/procedures/docker-manager.procedure.ts->src/core/database/repository.ts - - - - - -src/core/trpc/procedures/docker-manager.procedure.ts->src/core/trpc/trpc.ts - - - - - -src/core/trpc/procedures/docker-stats.procedure.ts->src/core/utils/logger.ts - - - - - -src/core/trpc/procedures/docker-stats.procedure.ts->src/core/database/repository.ts - - - - - -src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/docker.ts - - - - - -src/core/trpc/procedures/docker-stats.procedure.ts->src/core/docker/client.ts - - - - - -src/core/trpc/procedures/docker-stats.procedure.ts->src/core/utils/calculations.ts - - - - - -src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/dockerode.ts - - - - - -src/core/trpc/procedures/docker-stats.procedure.ts->src/core/trpc/trpc.ts - - - - - -src/core/trpc/procedures/logs.procedure.ts->src/core/utils/logger.ts - - - - - -src/core/trpc/procedures/logs.procedure.ts->src/core/database/repository.ts - - - - - -src/core/trpc/procedures/logs.procedure.ts->src/core/trpc/trpc.ts - - - - - -src/core/trpc/procedures/stacks.procedure.ts->src/core/utils/logger.ts - - - - - -src/core/trpc/procedures/stacks.procedure.ts->src/core/database/repository.ts - - - - - -src/core/trpc/procedures/stacks.procedure.ts->src/core/stacks/controller.ts - - - - - -src/core/trpc/procedures/stacks.procedure.ts->src/core/trpc/trpc.ts - - + + +src/core->src/typings + + - - -src/core/utils/respone-handler.ts - - -respone-handler.ts + + +stream + + +stream - - -src/core/utils/respone-handler.ts->src/core/utils/logger.ts - - + + +src/typings->stream + + - + src/index.ts - - -index.ts - - - - - -src/index.ts->src/core/utils/logger.ts - - - - - -src/index.ts->src/core/database/repository.ts - - - - - -src/index.ts->src/core/docker/scheduler.ts - - - - - -src/index.ts->src/core/plugins/loader.ts - - - - - -src/index.ts->src/core/trpc/index.ts - - - - - -src/routes/stacks.ts - - -stacks.ts + + +index.ts - - -src/index.ts->src/routes/stacks.ts - - - - - -src/routes/api-config.ts - - -api-config.ts - - - - - -src/index.ts->src/routes/api-config.ts - - - - - -src/routes/docker-manager.ts - - -docker-manager.ts - - - - - -src/index.ts->src/routes/docker-manager.ts - - - - - -src/routes/docker-stats.ts - - -docker-stats.ts - - - - - -src/index.ts->src/routes/docker-stats.ts - - - - - -src/routes/docker-websocket.ts - - -docker-websocket.ts - - - - - -src/index.ts->src/routes/docker-websocket.ts - - - - - -src/routes/logs.ts - - -logs.ts - - - - - -src/index.ts->src/routes/logs.ts - - - - - -src/routes/stacks.ts->src/core/utils/logger.ts - - - - - -src/routes/stacks.ts->src/core/database/repository.ts - - - - - -src/routes/stacks.ts->src/core/stacks/controller.ts - - - - - -src/routes/stacks.ts->src/core/utils/respone-handler.ts - - - - - -src/routes/api-config.ts->src/core/utils/logger.ts - - - - - -src/routes/api-config.ts->src/core/database/repository.ts - - - - - -src/routes/api-config.ts->src/typings/database.ts - - - - - -src/routes/api-config.ts->src/core/utils/package-json.ts - - - - - -src/routes/api-config.ts->src/core/utils/respone-handler.ts - - - - - -src/routes/docker-manager.ts->src/core/utils/logger.ts - - - - - -src/routes/docker-manager.ts->src/core/database/repository.ts - - - - - -src/routes/docker-manager.ts->src/core/utils/respone-handler.ts - - - - - -src/routes/docker-stats.ts->src/core/utils/logger.ts - - - - - -src/routes/docker-stats.ts->src/core/database/repository.ts - - - - - -src/routes/docker-stats.ts->src/typings/docker.ts - - - - - -src/routes/docker-stats.ts->src/core/docker/client.ts - - - - - -src/routes/docker-stats.ts->src/core/utils/calculations.ts - - - - - -src/routes/docker-stats.ts->src/typings/dockerode.ts - - - - - -src/routes/docker-stats.ts->src/core/utils/respone-handler.ts - - - - - -src/routes/docker-websocket.ts->src/core/utils/logger.ts - - - - - -src/routes/docker-websocket.ts->src/core/database/repository.ts - - - - - -src/routes/docker-websocket.ts->src/typings/docker.ts - - - - - -src/routes/docker-websocket.ts->src/core/docker/client.ts - - - - - -src/routes/docker-websocket.ts->src/core/utils/calculations.ts - - - - - -src/routes/docker-websocket.ts->src/core/utils/respone-handler.ts - - - - - -src/typings/websocket.ts - - -websocket.ts - - - - - -src/routes/docker-websocket.ts->src/typings/websocket.ts - - + + +src/index.ts->src/core + + - - -stream - - -stream + + +src/routes + + + + + +routes - - -src/routes/docker-websocket.ts->stream - - + + +src/index.ts->src/routes + + - - -src/routes/logs.ts->src/core/utils/logger.ts - - + + +src/routes->src/core + + - - -src/routes/logs.ts->src/core/database/repository.ts - - + + +src/routes->src/typings + + - - -src/typings/websocket.ts->stream - - + + +src/routes->stream + + From 93ef0c92e791b90ec7f1a7a524cf3ed19bde3cec Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 14 Mar 2025 00:19:18 +0100 Subject: [PATCH 186/324] Fix: Ortho --- .github/workflows/dependency-graph.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dependency-graph.yml b/.github/workflows/dependency-graph.yml index 7732876..03d23f1 100644 --- a/.github/workflows/dependency-graph.yml +++ b/.github/workflows/dependency-graph.yml @@ -31,8 +31,8 @@ jobs: - name: Generate Dependency Graph (SVG) run: | - bun run dependency-cruiser --output-type archi src/index.ts --output-to dependency-graph.dot --no-config -x node_modules --ts-pre-compilation-deps --ts-config tsconfig.json - dot -Tsvg dependency-graph.dot -o dependency-graph.svg + bun run dependency-cruiser --output-type dot src/index.ts --output-to dependency-graph.dot --no-config -x node_modules --ts-pre-compilation-deps --ts-config tsconfig.json + dot -T svg -Gsplines=ortho dependency-graph.dot -o dependency-graph.svg echo "SVG graph generated at dependency-graph.svg" - name: Commit and Push Changes From 538d54b0f8d72c5649984b209d88c6f4d9df627d Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Thu, 13 Mar 2025 23:19:43 +0000 Subject: [PATCH 187/324] Update dependency graphs --- dependency-graph.dot | 161 +++++- dependency-graph.svg | 1150 +++++++++++++++++++++++++++++++++++++----- 2 files changed, 1178 insertions(+), 133 deletions(-) diff --git a/dependency-graph.dot b/dependency-graph.dot index 277a1eb..4cf81c6 100644 --- a/dependency-graph.dot +++ b/dependency-graph.dot @@ -9,22 +9,151 @@ strict digraph "dependency-cruiser output"{ subgraph "cluster_fs" {label="fs" "fs/promises" [label= tooltip="promises" URL="https://nodejs.org/api/fs.html" color="grey" fontcolor="grey"] } "package.json" [label= tooltip="package.json" URL="package.json" fillcolor="#ffee44"] "path" [label= tooltip="path" URL="https://nodejs.org/api/path.html" color="grey" fontcolor="grey"] - subgraph "cluster_src" {label="src" "src/core" [label= tooltip="core" URL="src/core" shape="box3d"] } - "src/core" -> "src/typings" [arrowhead="onormal" penwidth="1.0"] - "src/core" -> "bun:sqlite" - "src/core" -> "path" [style="dashed" penwidth="1.0"] - "src/core" -> "fs" [style="dashed" penwidth="1.0"] - "src/core" -> "fs/promises" [style="dashed" penwidth="1.0"] - "src/core" -> "events" [style="dashed" penwidth="1.0"] - "src/core" -> "package.json" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/helper.ts" [label= tooltip="helper.ts" URL="src/core/database/helper.ts" fillcolor="#ddfeff"] } } } + "src/core/database/helper.ts" -> "src/core/utils/logger.ts" [arrowhead="normalnoneodot"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/repository.ts" [label= tooltip="repository.ts" URL="src/core/database/repository.ts" fillcolor="#ddfeff"] } } } + "src/core/database/repository.ts" -> "src/core/database/helper.ts" [arrowhead="normalnoneodot"] + "src/core/database/repository.ts" -> "src/core/utils/logger.ts" [arrowhead="normalnoneodot"] + "src/core/database/repository.ts" -> "src/typings/database.ts" [arrowhead="onormal" penwidth="1.0"] + "src/core/database/repository.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] + "src/core/database/repository.ts" -> "bun:sqlite" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/client.ts" [label= tooltip="client.ts" URL="src/core/docker/client.ts" fillcolor="#ddfeff"] } } } + "src/core/docker/client.ts" -> "src/core/utils/logger.ts" + "src/core/docker/client.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/scheduler.ts" [label= tooltip="scheduler.ts" URL="src/core/docker/scheduler.ts" fillcolor="#ddfeff"] } } } + "src/core/docker/scheduler.ts" -> "src/core/database/repository.ts" + "src/core/docker/scheduler.ts" -> "src/core/docker/store-host-stats.ts" + "src/core/docker/scheduler.ts" -> "src/core/docker/store-container-stats.ts" + "src/core/docker/scheduler.ts" -> "src/core/utils/logger.ts" + "src/core/docker/scheduler.ts" -> "src/typings/database.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/store-container-stats.ts" [label= tooltip="store-container-stats.ts" URL="src/core/docker/store-container-stats.ts" fillcolor="#ddfeff"] } } } + "src/core/docker/store-container-stats.ts" -> "src/core/database/repository.ts" + "src/core/docker/store-container-stats.ts" -> "src/core/docker/client.ts" + "src/core/docker/store-container-stats.ts" -> "src/core/utils/calculations.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/store-host-stats.ts" [label= tooltip="store-host-stats.ts" URL="src/core/docker/store-host-stats.ts" fillcolor="#ddfeff"] } } } + "src/core/docker/store-host-stats.ts" -> "src/core/database/repository.ts" + "src/core/docker/store-host-stats.ts" -> "src/core/docker/client.ts" + "src/core/docker/store-host-stats.ts" -> "src/core/utils/logger.ts" + "src/core/docker/store-host-stats.ts" -> "src/typings/docker.ts" + "src/core/docker/store-host-stats.ts" -> "src/typings/dockerode.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/plugins" {label="plugins" "src/core/plugins/loader.ts" [label= tooltip="loader.ts" URL="src/core/plugins/loader.ts" fillcolor="#ddfeff"] } } } + "src/core/plugins/loader.ts" -> "src/core/utils/change-me-checker.ts" + "src/core/plugins/loader.ts" -> "src/core/utils/logger.ts" + "src/core/plugins/loader.ts" -> "src/core/plugins/plugin-manager.ts" + "src/core/plugins/loader.ts" -> "fs" [style="dashed" penwidth="1.0"] + "src/core/plugins/loader.ts" -> "path" [style="dashed" penwidth="1.0"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/plugins" {label="plugins" "src/core/plugins/plugin-manager.ts" [label= tooltip="plugin-manager.ts" URL="src/core/plugins/plugin-manager.ts" fillcolor="#ddfeff"] } } } + "src/core/plugins/plugin-manager.ts" -> "src/core/utils/logger.ts" + "src/core/plugins/plugin-manager.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] + "src/core/plugins/plugin-manager.ts" -> "src/typings/plugin.ts" [arrowhead="onormal" penwidth="1.0"] + "src/core/plugins/plugin-manager.ts" -> "events" [style="dashed" penwidth="1.0"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/stacks" {label="stacks" "src/core/stacks/controller.ts" [label= tooltip="controller.ts" URL="src/core/stacks/controller.ts" fillcolor="#ddfeff"] } } } + "src/core/stacks/controller.ts" -> "src/core/database/repository.ts" + "src/core/stacks/controller.ts" -> "src/core/utils/logger.ts" + "src/core/stacks/controller.ts" -> "src/typings/database.ts" [arrowhead="onormal" penwidth="1.0"] + "src/core/stacks/controller.ts" -> "src/typings/docker-compose.ts" [arrowhead="onormal" penwidth="1.0"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" "src/core/trpc/index.ts" [label= tooltip="index.ts" URL="src/core/trpc/index.ts" fillcolor="#ddfeff"] } } } + "src/core/trpc/index.ts" -> "src/core/trpc/router.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/api-config.procedure.ts" [label= tooltip="api-config.procedure.ts" URL="src/core/trpc/procedures/api-config.procedure.ts" fillcolor="#ddfeff"] } } } } + "src/core/trpc/procedures/api-config.procedure.ts" -> "src/core/trpc/trpc.ts" + "src/core/trpc/procedures/api-config.procedure.ts" -> "src/core/database/repository.ts" + "src/core/trpc/procedures/api-config.procedure.ts" -> "src/core/utils/logger.ts" + "src/core/trpc/procedures/api-config.procedure.ts" -> "src/core/utils/package-json.ts" + "src/core/trpc/procedures/api-config.procedure.ts" -> "src/typings/database.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/docker-manager.procedure.ts" [label= tooltip="docker-manager.procedure.ts" URL="src/core/trpc/procedures/docker-manager.procedure.ts" fillcolor="#ddfeff"] } } } } + "src/core/trpc/procedures/docker-manager.procedure.ts" -> "src/core/trpc/trpc.ts" + "src/core/trpc/procedures/docker-manager.procedure.ts" -> "src/core/database/repository.ts" + "src/core/trpc/procedures/docker-manager.procedure.ts" -> "src/core/utils/logger.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/docker-stats.procedure.ts" [label= tooltip="docker-stats.procedure.ts" URL="src/core/trpc/procedures/docker-stats.procedure.ts" fillcolor="#ddfeff"] } } } } + "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/trpc/trpc.ts" + "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/database/repository.ts" + "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/docker/client.ts" + "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/utils/calculations.ts" + "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/utils/logger.ts" + "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] + "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/typings/dockerode.ts" [arrowhead="onormal" penwidth="1.0"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/logs.procedure.ts" [label= tooltip="logs.procedure.ts" URL="src/core/trpc/procedures/logs.procedure.ts" fillcolor="#ddfeff"] } } } } + "src/core/trpc/procedures/logs.procedure.ts" -> "src/core/trpc/trpc.ts" + "src/core/trpc/procedures/logs.procedure.ts" -> "src/core/database/repository.ts" + "src/core/trpc/procedures/logs.procedure.ts" -> "src/core/utils/logger.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/stacks.procedure.ts" [label= tooltip="stacks.procedure.ts" URL="src/core/trpc/procedures/stacks.procedure.ts" fillcolor="#ddfeff"] } } } } + "src/core/trpc/procedures/stacks.procedure.ts" -> "src/core/trpc/trpc.ts" + "src/core/trpc/procedures/stacks.procedure.ts" -> "src/core/database/repository.ts" + "src/core/trpc/procedures/stacks.procedure.ts" -> "src/core/stacks/controller.ts" + "src/core/trpc/procedures/stacks.procedure.ts" -> "src/core/utils/logger.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" "src/core/trpc/router.ts" [label= tooltip="router.ts" URL="src/core/trpc/router.ts" fillcolor="#ddfeff"] } } } + "src/core/trpc/router.ts" -> "src/core/trpc/procedures/api-config.procedure.ts" + "src/core/trpc/router.ts" -> "src/core/trpc/procedures/docker-manager.procedure.ts" + "src/core/trpc/router.ts" -> "src/core/trpc/procedures/docker-stats.procedure.ts" + "src/core/trpc/router.ts" -> "src/core/trpc/procedures/logs.procedure.ts" + "src/core/trpc/router.ts" -> "src/core/trpc/procedures/stacks.procedure.ts" + "src/core/trpc/router.ts" -> "src/core/trpc/trpc.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" "src/core/trpc/trpc.ts" [label= tooltip="trpc.ts" URL="src/core/trpc/trpc.ts" fillcolor="#ddfeff"] } } } + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/calculations.ts" [label= tooltip="calculations.ts" URL="src/core/utils/calculations.ts" fillcolor="#ddfeff"] } } } + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/change-me-checker.ts" [label= tooltip="change-me-checker.ts" URL="src/core/utils/change-me-checker.ts" fillcolor="#ddfeff"] } } } + "src/core/utils/change-me-checker.ts" -> "src/core/utils/logger.ts" + "src/core/utils/change-me-checker.ts" -> "fs/promises" [style="dashed" penwidth="1.0"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/logger.ts" [label= tooltip="logger.ts" URL="src/core/utils/logger.ts" fillcolor="#ddfeff"] } } } + "src/core/utils/logger.ts" -> "src/core/database/repository.ts" [arrowhead="normalnoneodot"] + "src/core/utils/logger.ts" -> "path" [style="dashed" penwidth="1.0"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/package-json.ts" [label= tooltip="package-json.ts" URL="src/core/utils/package-json.ts" fillcolor="#ddfeff"] } } } + "src/core/utils/package-json.ts" -> "package.json" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/respone-handler.ts" [label= tooltip="respone-handler.ts" URL="src/core/utils/respone-handler.ts" fillcolor="#ddfeff"] } } } + "src/core/utils/respone-handler.ts" -> "src/core/utils/logger.ts" subgraph "cluster_src" {label="src" "src/index.ts" [label= tooltip="index.ts" URL="src/index.ts" fillcolor="#ddfeff"] } - "src/index.ts" -> "src/routes" - "src/index.ts" -> "src/core" - subgraph "cluster_src" {label="src" "src/routes" [label= tooltip="routes" URL="src/routes" shape="box3d"] } - "src/routes" -> "src/core" - "src/routes" -> "src/typings" - "src/routes" -> "stream" [style="dashed" penwidth="1.0" arrowhead="onormal"] - subgraph "cluster_src" {label="src" "src/typings" [label= tooltip="typings" URL="src/typings" shape="box3d"] } - "src/typings" -> "stream" [style="dashed" penwidth="1.0" arrowhead="onormal"] + "src/index.ts" -> "src/routes/stacks.ts" + "src/index.ts" -> "src/core/database/repository.ts" + "src/index.ts" -> "src/core/docker/scheduler.ts" + "src/index.ts" -> "src/core/plugins/loader.ts" + "src/index.ts" -> "src/core/trpc/index.ts" + "src/index.ts" -> "src/core/utils/logger.ts" + "src/index.ts" -> "src/routes/api-config.ts" + "src/index.ts" -> "src/routes/docker-manager.ts" + "src/index.ts" -> "src/routes/docker-stats.ts" + "src/index.ts" -> "src/routes/docker-websocket.ts" + "src/index.ts" -> "src/routes/logs.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/api-config.ts" [label= tooltip="api-config.ts" URL="src/routes/api-config.ts" fillcolor="#ddfeff"] } } + "src/routes/api-config.ts" -> "src/core/database/repository.ts" + "src/routes/api-config.ts" -> "src/core/utils/logger.ts" + "src/routes/api-config.ts" -> "src/core/utils/package-json.ts" + "src/routes/api-config.ts" -> "src/core/utils/respone-handler.ts" + "src/routes/api-config.ts" -> "src/typings/database.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/docker-manager.ts" [label= tooltip="docker-manager.ts" URL="src/routes/docker-manager.ts" fillcolor="#ddfeff"] } } + "src/routes/docker-manager.ts" -> "src/core/database/repository.ts" + "src/routes/docker-manager.ts" -> "src/core/utils/logger.ts" + "src/routes/docker-manager.ts" -> "src/core/utils/respone-handler.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/docker-stats.ts" [label= tooltip="docker-stats.ts" URL="src/routes/docker-stats.ts" fillcolor="#ddfeff"] } } + "src/routes/docker-stats.ts" -> "src/core/database/repository.ts" + "src/routes/docker-stats.ts" -> "src/core/docker/client.ts" + "src/routes/docker-stats.ts" -> "src/core/utils/calculations.ts" + "src/routes/docker-stats.ts" -> "src/core/utils/logger.ts" + "src/routes/docker-stats.ts" -> "src/core/utils/respone-handler.ts" + "src/routes/docker-stats.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] + "src/routes/docker-stats.ts" -> "src/typings/dockerode.ts" [arrowhead="onormal" penwidth="1.0"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/docker-websocket.ts" [label= tooltip="docker-websocket.ts" URL="src/routes/docker-websocket.ts" fillcolor="#ddfeff"] } } + "src/routes/docker-websocket.ts" -> "src/core/database/repository.ts" + "src/routes/docker-websocket.ts" -> "src/core/docker/client.ts" + "src/routes/docker-websocket.ts" -> "src/core/utils/calculations.ts" + "src/routes/docker-websocket.ts" -> "src/core/utils/logger.ts" + "src/routes/docker-websocket.ts" -> "src/core/utils/respone-handler.ts" + "src/routes/docker-websocket.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] + "src/routes/docker-websocket.ts" -> "src/typings/websocket.ts" [arrowhead="onormal" penwidth="1.0"] + "src/routes/docker-websocket.ts" -> "stream" [style="dashed" penwidth="1.0" arrowhead="onormal"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/logs.ts" [label= tooltip="logs.ts" URL="src/routes/logs.ts" fillcolor="#ddfeff"] } } + "src/routes/logs.ts" -> "src/core/database/repository.ts" + "src/routes/logs.ts" -> "src/core/utils/logger.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/stacks.ts" [label= tooltip="stacks.ts" URL="src/routes/stacks.ts" fillcolor="#ddfeff"] } } + "src/routes/stacks.ts" -> "src/core/database/repository.ts" + "src/routes/stacks.ts" -> "src/core/stacks/controller.ts" + "src/routes/stacks.ts" -> "src/core/utils/logger.ts" + "src/routes/stacks.ts" -> "src/core/utils/respone-handler.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/database.ts" [label= tooltip="database.ts" URL="src/typings/database.ts" fillcolor="#ddfeff"] } } + subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/docker-compose.ts" [label= tooltip="docker-compose.ts" URL="src/typings/docker-compose.ts" fillcolor="#ddfeff"] } } + subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/docker.ts" [label= tooltip="docker.ts" URL="src/typings/docker.ts" fillcolor="#ddfeff"] } } + subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/dockerode.ts" [label= tooltip="dockerode.ts" URL="src/typings/dockerode.ts" fillcolor="#ddfeff"] } } + subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/plugin.ts" [label= tooltip="plugin.ts" URL="src/typings/plugin.ts" fillcolor="#ddfeff"] } } + "src/typings/plugin.ts" -> "src/typings/docker.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/websocket.ts" [label= tooltip="websocket.ts" URL="src/typings/websocket.ts" fillcolor="#ddfeff"] } } + "src/typings/websocket.ts" -> "stream" [style="dashed" penwidth="1.0" arrowhead="onormal"] "stream" [label= tooltip="stream" URL="https://nodejs.org/api/stream.html" color="grey" fontcolor="grey"] } diff --git a/dependency-graph.svg b/dependency-graph.svg index c833eb6..ada00d6 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -4,27 +4,77 @@ - - + + dependency-cruiser output - + cluster_fs - -fs + +fs cluster_src - -src + +src + + +cluster_src/core + +core + + +cluster_src/core/database + +database + + +cluster_src/core/docker + +docker + + +cluster_src/core/plugins + +plugins + + +cluster_src/core/stacks + +stacks + + +cluster_src/core/trpc + +trpc + + +cluster_src/core/trpc/procedures + +procedures + + +cluster_src/core/utils + +utils + + +cluster_src/routes + +routes + + +cluster_src/typings + +typings bun:sqlite - -bun:sqlite + +bun:sqlite @@ -32,8 +82,8 @@ events - -events + +events @@ -41,8 +91,8 @@ fs - -fs + +fs @@ -50,8 +100,8 @@ fs/promises - -promises + +promises @@ -59,8 +109,8 @@ package.json - -package.json + +package.json @@ -68,142 +118,1008 @@ path - -path + +path - + -src/core - - - - - -core +src/core/database/helper.ts + + +helper.ts - - -src/core->bun:sqlite - - + + +src/core/utils/logger.ts + + +logger.ts + + - + + +src/core/database/helper.ts->src/core/utils/logger.ts + + + + + + + +src/core/utils/logger.ts->path + + + + + +src/core/database/repository.ts + + +repository.ts + + + + + +src/core/utils/logger.ts->src/core/database/repository.ts + + + + + + -src/core->events - - +src/core/database/repository.ts->bun:sqlite + + + + + +src/core/database/repository.ts->src/core/database/helper.ts + + + + + + + +src/core/database/repository.ts->src/core/utils/logger.ts + + + + + + + +src/typings/database.ts + + +database.ts + - + + -src/core->fs - - +src/core/database/repository.ts->src/typings/database.ts + + + + + +src/typings/docker.ts + + +docker.ts + - + + -src/core->fs/promises - - +src/core/database/repository.ts->src/typings/docker.ts + + + + + +src/core/docker/client.ts + + +client.ts + + - + -src/core->package.json - - +src/core/docker/client.ts->src/core/utils/logger.ts + + - - -src/core->path - - + + +src/core/docker/client.ts->src/typings/docker.ts + + - - -src/typings - - - - - -typings + + +src/core/docker/scheduler.ts + + +scheduler.ts - - -src/core->src/typings - - + + +src/core/docker/scheduler.ts->src/core/utils/logger.ts + + - - -stream - - -stream + + +src/core/docker/scheduler.ts->src/core/database/repository.ts + + + + + +src/core/docker/scheduler.ts->src/typings/database.ts + + + + + +src/core/docker/store-host-stats.ts + + +store-host-stats.ts - - -src/typings->stream - - + + +src/core/docker/scheduler.ts->src/core/docker/store-host-stats.ts + + + + + +src/core/docker/store-container-stats.ts + + +store-container-stats.ts + + + + + +src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts + + + + + +src/core/docker/store-host-stats.ts->src/core/utils/logger.ts + + + + + +src/core/docker/store-host-stats.ts->src/core/database/repository.ts + + + + + +src/core/docker/store-host-stats.ts->src/typings/docker.ts + + + + + +src/core/docker/store-host-stats.ts->src/core/docker/client.ts + + + + + +src/typings/dockerode.ts + + +dockerode.ts + + + + + +src/core/docker/store-host-stats.ts->src/typings/dockerode.ts + + + + + +src/core/docker/store-container-stats.ts->src/core/database/repository.ts + + + + + +src/core/docker/store-container-stats.ts->src/core/docker/client.ts + + + + + +src/core/utils/calculations.ts + + +calculations.ts + + + + + +src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts + + + + + +src/core/plugins/loader.ts + + +loader.ts + + + + + +src/core/plugins/loader.ts->fs + + + + + +src/core/plugins/loader.ts->path + + + + + +src/core/plugins/loader.ts->src/core/utils/logger.ts + + + + + +src/core/utils/change-me-checker.ts + + +change-me-checker.ts + + + + + +src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts + + + + + +src/core/plugins/plugin-manager.ts + + +plugin-manager.ts + + + + + +src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts + + + + + +src/core/utils/change-me-checker.ts->fs/promises + + + + + +src/core/utils/change-me-checker.ts->src/core/utils/logger.ts + + + + + +src/core/plugins/plugin-manager.ts->events + + + + + +src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts + + + + + +src/core/plugins/plugin-manager.ts->src/typings/docker.ts + + + + + +src/typings/plugin.ts + + +plugin.ts + + + + + +src/core/plugins/plugin-manager.ts->src/typings/plugin.ts + + + + + +src/typings/plugin.ts->src/typings/docker.ts + + + + + +src/core/stacks/controller.ts + + +controller.ts + + + + + +src/core/stacks/controller.ts->src/core/utils/logger.ts + + + + + +src/core/stacks/controller.ts->src/core/database/repository.ts + + + + + +src/core/stacks/controller.ts->src/typings/database.ts + + + + + +src/typings/docker-compose.ts + + +docker-compose.ts + + + + + +src/core/stacks/controller.ts->src/typings/docker-compose.ts + + + + + +src/core/trpc/index.ts + + +index.ts + + + + + +src/core/trpc/router.ts + + +router.ts + + + + + +src/core/trpc/index.ts->src/core/trpc/router.ts + + + + + +src/core/trpc/procedures/api-config.procedure.ts + + +api-config.procedure.ts + + + + + +src/core/trpc/router.ts->src/core/trpc/procedures/api-config.procedure.ts + + + + + +src/core/trpc/trpc.ts + + +trpc.ts + + + + + +src/core/trpc/router.ts->src/core/trpc/trpc.ts + + + + + +src/core/trpc/procedures/docker-manager.procedure.ts + + +docker-manager.procedure.ts + + + + + +src/core/trpc/router.ts->src/core/trpc/procedures/docker-manager.procedure.ts + + + + + +src/core/trpc/procedures/docker-stats.procedure.ts + + +docker-stats.procedure.ts + + + + + +src/core/trpc/router.ts->src/core/trpc/procedures/docker-stats.procedure.ts + + + + + +src/core/trpc/procedures/logs.procedure.ts + + +logs.procedure.ts + + + + + +src/core/trpc/router.ts->src/core/trpc/procedures/logs.procedure.ts + + + + + +src/core/trpc/procedures/stacks.procedure.ts + + +stacks.procedure.ts + + + + + +src/core/trpc/router.ts->src/core/trpc/procedures/stacks.procedure.ts + + + + + +src/core/trpc/procedures/api-config.procedure.ts->src/core/utils/logger.ts + + + + + +src/core/trpc/procedures/api-config.procedure.ts->src/core/database/repository.ts + + + + + +src/core/trpc/procedures/api-config.procedure.ts->src/typings/database.ts + + + + + +src/core/trpc/procedures/api-config.procedure.ts->src/core/trpc/trpc.ts + + + + + +src/core/utils/package-json.ts + + +package-json.ts + + + + + +src/core/trpc/procedures/api-config.procedure.ts->src/core/utils/package-json.ts + + + + + +src/core/utils/package-json.ts->package.json + + + + + +src/core/trpc/procedures/docker-manager.procedure.ts->src/core/utils/logger.ts + + + + + +src/core/trpc/procedures/docker-manager.procedure.ts->src/core/database/repository.ts + + + + + +src/core/trpc/procedures/docker-manager.procedure.ts->src/core/trpc/trpc.ts + + + + + +src/core/trpc/procedures/docker-stats.procedure.ts->src/core/utils/logger.ts + + + + + +src/core/trpc/procedures/docker-stats.procedure.ts->src/core/database/repository.ts + + + + + +src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/docker.ts + + + + + +src/core/trpc/procedures/docker-stats.procedure.ts->src/core/docker/client.ts + + + + + +src/core/trpc/procedures/docker-stats.procedure.ts->src/core/utils/calculations.ts + + + + + +src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/dockerode.ts + + + + + +src/core/trpc/procedures/docker-stats.procedure.ts->src/core/trpc/trpc.ts + + + + + +src/core/trpc/procedures/logs.procedure.ts->src/core/utils/logger.ts + + + + + +src/core/trpc/procedures/logs.procedure.ts->src/core/database/repository.ts + + + + + +src/core/trpc/procedures/logs.procedure.ts->src/core/trpc/trpc.ts + + + + + +src/core/trpc/procedures/stacks.procedure.ts->src/core/utils/logger.ts + + + + + +src/core/trpc/procedures/stacks.procedure.ts->src/core/database/repository.ts + + + + + +src/core/trpc/procedures/stacks.procedure.ts->src/core/stacks/controller.ts + + + + + +src/core/trpc/procedures/stacks.procedure.ts->src/core/trpc/trpc.ts + + + + + +src/core/utils/respone-handler.ts + + +respone-handler.ts + + + + + +src/core/utils/respone-handler.ts->src/core/utils/logger.ts + + - + src/index.ts - - -index.ts + + +index.ts - - -src/index.ts->src/core - - + + +src/index.ts->src/core/utils/logger.ts + + - - -src/routes - - - - - -routes + + +src/index.ts->src/core/database/repository.ts + + + + + +src/index.ts->src/core/docker/scheduler.ts + + + + + +src/index.ts->src/core/plugins/loader.ts + + + + + +src/index.ts->src/core/trpc/index.ts + + + + + +src/routes/stacks.ts + + +stacks.ts - - -src/index.ts->src/routes - - + + +src/index.ts->src/routes/stacks.ts + + - - -src/routes->src/core - - + + +src/routes/api-config.ts + + +api-config.ts + - - -src/routes->src/typings - - - - -src/routes->stream - - + + +src/index.ts->src/routes/api-config.ts + + + + + +src/routes/docker-manager.ts + + +docker-manager.ts + + + + + +src/index.ts->src/routes/docker-manager.ts + + + + + +src/routes/docker-stats.ts + + +docker-stats.ts + + + + + +src/index.ts->src/routes/docker-stats.ts + + + + + +src/routes/docker-websocket.ts + + +docker-websocket.ts + + + + + +src/index.ts->src/routes/docker-websocket.ts + + + + + +src/routes/logs.ts + + +logs.ts + + + + + +src/index.ts->src/routes/logs.ts + + + + + +src/routes/stacks.ts->src/core/utils/logger.ts + + + + + +src/routes/stacks.ts->src/core/database/repository.ts + + + + + +src/routes/stacks.ts->src/core/stacks/controller.ts + + + + + +src/routes/stacks.ts->src/core/utils/respone-handler.ts + + + + + +src/routes/api-config.ts->src/core/utils/logger.ts + + + + + +src/routes/api-config.ts->src/core/database/repository.ts + + + + + +src/routes/api-config.ts->src/typings/database.ts + + + + + +src/routes/api-config.ts->src/core/utils/package-json.ts + + + + + +src/routes/api-config.ts->src/core/utils/respone-handler.ts + + + + + +src/routes/docker-manager.ts->src/core/utils/logger.ts + + + + + +src/routes/docker-manager.ts->src/core/database/repository.ts + + + + + +src/routes/docker-manager.ts->src/core/utils/respone-handler.ts + + + + + +src/routes/docker-stats.ts->src/core/utils/logger.ts + + + + + +src/routes/docker-stats.ts->src/core/database/repository.ts + + + + + +src/routes/docker-stats.ts->src/typings/docker.ts + + + + + +src/routes/docker-stats.ts->src/core/docker/client.ts + + + + + +src/routes/docker-stats.ts->src/core/utils/calculations.ts + + + + + +src/routes/docker-stats.ts->src/typings/dockerode.ts + + + + + +src/routes/docker-stats.ts->src/core/utils/respone-handler.ts + + + + + +src/routes/docker-websocket.ts->src/core/utils/logger.ts + + + + + +src/routes/docker-websocket.ts->src/core/database/repository.ts + + + + + +src/routes/docker-websocket.ts->src/typings/docker.ts + + + + + +src/routes/docker-websocket.ts->src/core/docker/client.ts + + + + + +src/routes/docker-websocket.ts->src/core/utils/calculations.ts + + + + + +src/routes/docker-websocket.ts->src/core/utils/respone-handler.ts + + + + + +src/typings/websocket.ts + + +websocket.ts + + + + + +src/routes/docker-websocket.ts->src/typings/websocket.ts + + + + + +stream + + +stream + + + + + +src/routes/docker-websocket.ts->stream + + + + + +src/routes/logs.ts->src/core/utils/logger.ts + + + + + +src/routes/logs.ts->src/core/database/repository.ts + + + + + +src/typings/websocket.ts->stream + + From 3e851c91e83351c40df633f56da1a018ce687378 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 18 Mar 2025 09:14:01 +0100 Subject: [PATCH 188/324] Fix: Add dependency-graphs to Readme --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 87b7750..87fea6f 100644 --- a/README.md +++ b/README.md @@ -29,3 +29,11 @@ Docker monitoring API with real-time statistics, stack management, and plugin su ## Documentation and Wiki Please see [DockStatAPI](https://dockstatapi.itsnik.de) + +## Project Graph + +### SVG: + +![Dependency Graph](./dependency-graph.svg) + +Click [here](./dependency-graph.mmd) for the mermaid version From 83e52e4ac2670fa9327723249dac88cceaaf7f2a Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 18 Mar 2025 12:31:37 +0100 Subject: [PATCH 189/324] Feat: Authentication, closes #40 and some more adjustments --- src/core/database/repository.ts | 161 ++++++++++-------- .../trpc/procedures/api-config.procedure.ts | 5 +- src/core/utils/respone-handler.ts | 11 +- src/index.ts | 54 +++++- src/middleware/auth.ts | 78 +++++++++ src/routes/api-config.ts | 24 ++- src/typings/database.ts | 1 + src/typings/elysiajs.ts | 12 ++ 8 files changed, 249 insertions(+), 97 deletions(-) create mode 100644 src/middleware/auth.ts create mode 100644 src/typings/elysiajs.ts diff --git a/src/core/database/repository.ts b/src/core/database/repository.ts index 37f6ba5..d5f11d6 100644 --- a/src/core/database/repository.ts +++ b/src/core/database/repository.ts @@ -2,9 +2,9 @@ import { executeDbOperation } from "./helper"; import Database from "bun:sqlite"; import { logger } from "~/core/utils/logger"; import type { DockerHost, HostStats } from "~/typings/docker"; -import type { stacks_config } from "~/typings/database"; +import type { config, stacks_config } from "~/typings/database"; -const db = new Database("dockstatapi.db"); +const db = new Database("dockstatapi.db", { strict: true }); db.exec("PRAGMA journal_mode = WAL;"); export const dbFunctions = { @@ -13,60 +13,61 @@ export const dbFunctions = { db.exec(` CREATE TABLE IF NOT EXISTS backend_log_entries ( timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, - level TEXT, - message TEXT, - file TEXT, - line NUMBER + level TEXT NOT NULL, + message TEXT NOT NULL, + file TEXT NOT NULL, + line NUMBER NOT NULL ); CREATE TABLE IF NOT EXISTS stacks_config ( - name TEXT PRIMARY KEY, - version INTEGER, - custom BOOLEAN, - source TEXT, - container_count INTEGER, - stack_prefix TEXT, - automatic_reboot_on_error BOOLEAN, - image_updates BOOLEAN + name TEXT PRIMARY KEY NOT NULL, + version INTEGER NOT NULL, + custom BOOLEAN NOT NULL, + source TEXT NOT NULL, + container_count INTEGER NOT NULL, + stack_prefix TEXT NOT NULL, + automatic_reboot_on_error BOOLEAN NOT NULL, + image_updates BOOLEAN NOT NULL ); CREATE TABLE IF NOT EXISTS docker_hosts ( - name TEXT, - url TEXT, - secure BOOLEAN + name TEXT NOT NULL, + url TEXT NOT NULL, + secure BOOLEAN NOT NULL ); CREATE TABLE IF NOT EXISTS host_stats ( - hostId TEXT PRIMARY KEY, - dockerVersion TEXT, - apiVersion TEXT, - os TEXT, - architecture TEXT, - totalMemory INTEGER, - totalCPU INTEGER, - labels TEXT, - containers INTEGER, - containersRunning INTEGER, - containersStopped INTEGER, - containersPaused INTEGER, - images INTEGER + hostId TEXT PRIMARY KEY NOT NULL, + dockerVersion TEXT NOT NULL, + apiVersion TEXT NOT NULL, + os TEXT NOT NULL, + architecture TEXT NOT NULL, + totalMemory INTEGER NOT NULL, + totalCPU INTEGER NOT NULL, + labels TEXT NOT NULL, + containers INTEGER NOT NULL, + containersRunning INTEGER NOT NULL, + containersStopped INTEGER NOT NULL, + containersPaused INTEGER NOT NULL, + images INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS container_stats ( - id TEXT, - hostId TEXT, - name TEXT, - image TEXT, - status TEXT, - state TEXT, - cpu_usage FLOAT, - memory_usage FLOAT, + id TEXT NOT NULL, + hostId TEXT NOT NULL, + name TEXT NOT NULL, + image TEXT NOT NULL, + status TEXT NOT NULL, + state TEXT NOT NULL, + cpu_usage FLOAT NOT NULL, + memory_usage FLOAT NOT NULL, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS config ( - keep_data_for NUMBER, - fetching_interval NUMBER + keep_data_for NUMBER NOT NULL, + fetching_interval NUMBER NOT NULL, + api_key TEXT NOT NULL ); `); @@ -76,6 +77,7 @@ export const dbFunctions = { * Default values: * - Data retention value for the database (logs and container stats) 7 days * - Data fetcher for the Database: 5 minutes + * - api_key: changeme */ const configRow = db .prepare(`SELECT COUNT(*) AS count FROM config`) @@ -84,8 +86,8 @@ export const dbFunctions = { logger.debug("Initializing default config"); const stmt = db.prepare( ` - INSERT INTO config (keep_data_for, fetching_interval) VALUES (7, 5) - ` + INSERT INTO config (keep_data_for, fetching_interval, api_key) VALUES (7, 5, "changeme") + `, ); stmt.run(); } @@ -98,7 +100,7 @@ export const dbFunctions = { const stmt = db.prepare( ` INSERT INTO docker_hosts (name, url, secure) VALUES (?, ?, ?) - ` + `, ); stmt.run("Localhost", "localhost:2375", false); } @@ -118,6 +120,20 @@ export const dbFunctions = { return stmt.run(hostId, url, secure); }, () => { + if (hostId.length < 1) { + logger.error("Hostname needed"); + throw new Error( + "Invalid data provided, please see server's log for more info", + ); + } + + if (url.length < 1) { + logger.error("URL needed"); + throw new Error( + "Invalid data provided, please see server's log for more info", + ); + } + if ( typeof hostId !== "string" || typeof url !== "string" || @@ -126,7 +142,7 @@ export const dbFunctions = { logger.error("Invalid parameter types for addDockerHost"); throw new TypeError("Invalid parameter types for addDockerHost"); } - } + }, ); }, @@ -142,7 +158,7 @@ export const dbFunctions = { const data = stmt.all(); return data as DockerHost[]; }, - () => {} + () => {}, ); }, @@ -150,7 +166,7 @@ export const dbFunctions = { level: string, message: string, file_name: string, - line: number + line: number, ) => { if ( typeof level !== "string" || @@ -181,7 +197,7 @@ export const dbFunctions = { const data = stmt.all(); return data; }, - () => {} + () => {}, ); }, @@ -203,7 +219,7 @@ export const dbFunctions = { logger.error("Level parameter must be a string"); throw new TypeError("Level parameter must be a string"); } - } + }, ); }, @@ -228,7 +244,7 @@ export const dbFunctions = { logger.error("Invalid parameter types for updateDockerHost"); throw new TypeError("Invalid parameter types for updateDockerHost"); } - } + }, ); }, @@ -248,7 +264,7 @@ export const dbFunctions = { logger.error("Invalid parameter type for deleteDockerHost"); throw new TypeError("Name parameter must be a string"); } - } + }, ); }, @@ -262,7 +278,7 @@ export const dbFunctions = { const data = stmt.run(); return data; }, - () => {} + () => {}, ); }, @@ -282,20 +298,25 @@ export const dbFunctions = { logger.error("Invalid parameter type for clearLogsByLevel"); throw new TypeError("Level parameter must be a string"); } - } + }, ); }, - updateConfig(fetching_interval: number, keep_data_for: number) { + updateConfig( + fetching_interval: number, + keep_data_for: number, + api_key: string, + ) { return executeDbOperation( "Update Config", () => { const stmt = db.prepare(` UPDATE config SET fetching_interval = ?, - keep_data_for = ? + keep_data_for = ?, + api_key = ? `); - const data = stmt.run(fetching_interval, keep_data_for); + const data = stmt.run(fetching_interval, keep_data_for, api_key); return data; }, () => { @@ -306,7 +327,7 @@ export const dbFunctions = { logger.error("Invalid parameter types for updateConfig"); throw new TypeError("Invalid parameter types for updateConfig"); } - } + }, ); }, @@ -315,13 +336,13 @@ export const dbFunctions = { "Get Config", () => { const stmt = db.prepare(` - SELECT keep_data_for, fetching_interval + SELECT keep_data_for, fetching_interval, api_key FROM config `); const data = stmt.all(); return data; }, - () => {} + () => {}, ); }, @@ -346,7 +367,7 @@ export const dbFunctions = { logger.error("Invalid parameter type for deleteOldData"); throw new TypeError("Days parameter must be a number"); } - } + }, ); }, @@ -358,7 +379,7 @@ export const dbFunctions = { status: string, state: string, cpu_usage: number, - memory_usage: number + memory_usage: number, ) { return executeDbOperation( "Add Container Stats", @@ -375,7 +396,7 @@ export const dbFunctions = { status, state, cpu_usage, - memory_usage + memory_usage, ); return data; }, @@ -393,7 +414,7 @@ export const dbFunctions = { logger.error("Invalid parameter types for addContainerStats"); throw new TypeError("Invalid parameter types for addContainerStats"); } - } + }, ); }, @@ -446,11 +467,11 @@ export const dbFunctions = { stats.containersRunning, stats.containersStopped, stats.containersPaused, - stats.images + stats.images, ); return data; }, - () => {} + () => {}, ); }, @@ -479,11 +500,11 @@ export const dbFunctions = { stack_config.container_count, stack_config.stack_prefix, stack_config.automatic_reboot_on_error, - stack_config.image_updates + stack_config.image_updates, ); return data; }, - () => {} + () => {}, ); }, @@ -499,7 +520,7 @@ export const dbFunctions = { const data = stmt.all(); return data; }, - () => {} + () => {}, ); }, @@ -514,7 +535,7 @@ export const dbFunctions = { const data = stmt.run(name); return data; }, - () => {} + () => {}, ); }, @@ -542,11 +563,11 @@ export const dbFunctions = { stack_config.stack_prefix, stack_config.automatic_reboot_on_error, stack_config.image_updates, - stack_config.name + stack_config.name, ); return data; }, - () => {} + () => {}, ); }, }; diff --git a/src/core/trpc/procedures/api-config.procedure.ts b/src/core/trpc/procedures/api-config.procedure.ts index bf6cd40..6b3b248 100644 --- a/src/core/trpc/procedures/api-config.procedure.ts +++ b/src/core/trpc/procedures/api-config.procedure.ts @@ -19,6 +19,7 @@ import { config } from "~/typings/database"; const configInputSchema = z.object({ fetching_interval: z.number(), keep_data_for: z.number(), + api_key: z.string(), }); export const configProcedure = router({ @@ -40,8 +41,8 @@ export const configProcedure = router({ update: publicProcedure.input(configInputSchema).mutation(({ input }) => { try { - const { fetching_interval, keep_data_for } = input; - dbFunctions.updateConfig(fetching_interval, keep_data_for); + const { fetching_interval, keep_data_for, api_key } = input; + dbFunctions.updateConfig(fetching_interval, keep_data_for, api_key); return { success: true, message: "Updated DockStatAPI config" }; } catch (error) { logger.error("tRPC config update error", error); diff --git a/src/core/utils/respone-handler.ts b/src/core/utils/respone-handler.ts index 93e0cdb..65b7c09 100644 --- a/src/core/utils/respone-handler.ts +++ b/src/core/utils/respone-handler.ts @@ -1,14 +1,5 @@ import { logger } from "~/core/utils/logger"; -import type { HTTPHeaders } from "elysia/dist/types"; -import type { ElysiaCookie } from "elysia/dist/cookies"; -import type { StatusMap } from "elysia"; - -interface set { - headers: HTTPHeaders; - status?: number | keyof StatusMap; - redirect?: string; - cookie?: Record; -} +import type { set } from "~/typings/elysiajs"; export const responseHandler = { error( diff --git a/src/index.ts b/src/index.ts index 482f924..7aa4702 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,12 +13,15 @@ import { setSchedules } from "~/core/docker/scheduler"; import { serverTiming } from "@elysiajs/server-timing"; import staticPlugin from "@elysiajs/static"; import trpcRouter from "~/core/trpc"; +import { config } from "./typings/database"; +import { validateApiKey } from "./middleware/auth"; console.log(""); dbFunctions.init(); const DockStatAPI = new Elysia() .use(staticPlugin()) + .use(serverTiming()) .use( swagger({ documentation: { @@ -27,6 +30,21 @@ const DockStatAPI = new Elysia() version: "2.1.0", description: "Docker monitoring API with plugin support", }, + components: { + securitySchemes: { + apiKeyAuth: { + type: "apiKey", + name: "x-api-key", + in: "header", + description: "API key for authentication", + }, + }, + }, + security: [ + { + apiKeyAuth: [], + }, + ], tags: [ { name: "Statistics", @@ -47,9 +65,24 @@ const DockStatAPI = new Elysia() }, ], }, - }) + }), ) - .use(serverTiming()) + .onBeforeHandle(async (context) => { + const { path, request, set } = context; + + if (path === "/health" || path.startsWith("/swagger")) { + logger.info(`Requested unguarded route: ${path}`); + return; + } + + const validation = await validateApiKey(request, set); + + if (validation.error) { + set.status = 400; + set.headers["Content-Type"] = "application/json"; + return { error: validation.error }; + } + }) .use(trpcRouter) .use(dockerRoutes) .use(dockerStatsRoutes) @@ -58,9 +91,9 @@ const DockStatAPI = new Elysia() .use(apiConfigRoutes) .use(stackRoutes) .get("/health", () => ({ status: "healthy" }), { tags: ["Utils"] }) - .onError(({ code, set }) => { + .onError(({ code, set, path }) => { if (code === "NOT_FOUND") { - logger.warn("Unknown route, showing error page!"); + logger.warn(`Unknown route (${path}), showing error page!`); set.status = 404; set.headers["Content-Type"] = "text/html"; return Bun.file("public/404.html"); @@ -70,14 +103,23 @@ const DockStatAPI = new Elysia() async function startServer() { try { await loadPlugins("./src/plugins"); + const configData = dbFunctions.getConfig() as config[]; + const apiKey = configData[0].api_key; + + if (apiKey === "changeme") { + logger.warn( + "Default API Key of 'changeme' detected. Please change your API Key via the `/config/update` route!", + ); + } + DockStatAPI.listen(3000, ({ hostname, port }) => { console.log("----- [ ############## ]"); logger.info(`DockStatAPI is running at http://${hostname}:${port}`); logger.info( - `Swagger API Documentation available at http://${hostname}:${port}/swagger` + `Swagger API Documentation available at http://${hostname}:${port}/swagger`, ); logger.info( - `tRPC Endpoint available at: http://${hostname}:${port}/trpc` + `tRPC Endpoint available at: http://${hostname}:${port}/trpc`, ); }); } catch (error) { diff --git a/src/middleware/auth.ts b/src/middleware/auth.ts new file mode 100644 index 0000000..ac0c860 --- /dev/null +++ b/src/middleware/auth.ts @@ -0,0 +1,78 @@ +import { dbFunctions } from "~/core/database/repository"; +import { logger } from "~/core/utils/logger"; +import { config } from "~/typings/database"; +import { set } from "~/typings/elysiajs"; + +export async function hashApiKey(apiKey: string): Promise { + logger.debug("Hashing API key"); + try { + logger.debug("API key hashed successfully"); + return await Bun.password.hash(apiKey); + } catch (error) { + logger.error("Error hashing API key", error); + throw new Error("Failed to hash API key"); + } +} + +async function validateApiKeyHash( + providedKey: string, + storedHash: string, +): Promise { + logger.debug("Validating API key hash"); + try { + const isValid = await Bun.password.verify(providedKey, storedHash); + logger.debug(`API key validation result: ${isValid}`); + return isValid; + } catch (error) { + logger.error("Error validating API key hash", error); + return false; + } +} + +async function getApiKeyFromDb( + apiKey: string, +): Promise<{ hash: string } | null> { + const dbApiKey = (dbFunctions.getConfig() as config[])[0].api_key; + logger.debug(`Querying database for API key: ${apiKey}`); + return Promise.resolve({ + hash: dbApiKey, + }); +} + +export async function validateApiKey(request: Request, set: set) { + const apiKey = request.headers.get("x-api-key"); + logger.debug(`API key validation initiated`); + + if (process.env.NODE_ENV != "production") { + return { apiKey }; + } else if (!apiKey) { + logger.error(`API key missing from request ${request.url}`); + set.status = 401; + return { error: "API key required" }; + } + + try { + const dbRecord = await getApiKeyFromDb(apiKey); + + if (!dbRecord) { + logger.error("API key not found in database"); + set.status = 401; + return { error: "Invalid API key" }; + } + + const isValid = await validateApiKeyHash(apiKey, dbRecord.hash); + + if (!isValid) { + logger.error("Invalid API key provided"); + set.status = 401; + return { error: "Invalid API key" }; + } + + logger.info(`Valid API key used: ${apiKey}`); + return { apiKey }; + } catch (error) { + logger.error("Error during API key validation", error); + set.status = 500; + return { error: "Internal server error" }; + } +} diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index bc08132..1c3b13b 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -14,10 +14,11 @@ import { devDependencies, license, } from "~/core/utils/package-json"; +import { hashApiKey } from "~/middleware/auth"; export const apiConfigRoutes = new Elysia({ prefix: "/config" }) .get( - "/get", + "/", async ({ set }) => { try { const data = dbFunctions.getConfig() as config[]; @@ -30,27 +31,31 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) return responseHandler.error( set, "Error getting the DockStatAPI config", - error as string + error as string, ); } }, { tags: ["Management"], - } + }, ) .post( "/update", async ({ set, body }) => { try { - const { fetching_interval, keep_data_for } = body; + const { fetching_interval, keep_data_for, api_key } = body; set.headers["Content-Type"] = "application/json"; - dbFunctions.updateConfig(fetching_interval, keep_data_for); + dbFunctions.updateConfig( + fetching_interval, + keep_data_for, + await hashApiKey(api_key), + ); return responseHandler.ok(set, "Updated DockStatAPI config"); } catch (error) { return responseHandler.error( set, "Error updating the DockStatAPI config", - error as string + error as string, ); } }, @@ -58,9 +63,10 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) body: t.Object({ fetching_interval: t.Number(), keep_data_for: t.Number(), + api_key: t.String(), }), tags: ["Management"], - } + }, ) .get( "/package", @@ -82,11 +88,11 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) return responseHandler.error( set, error as string, - "Error while reading package.json" + "Error while reading package.json", ); } }, { tags: ["Management"], - } + }, ); diff --git a/src/typings/database.ts b/src/typings/database.ts index c5200e6..c9b15d3 100644 --- a/src/typings/database.ts +++ b/src/typings/database.ts @@ -9,6 +9,7 @@ interface backend_log_entries { interface config { keep_data_for: number; fetching_interval: number; + api_key: string; } interface stacks_config { diff --git a/src/typings/elysiajs.ts b/src/typings/elysiajs.ts new file mode 100644 index 0000000..913ceea --- /dev/null +++ b/src/typings/elysiajs.ts @@ -0,0 +1,12 @@ +import type { StatusMap } from "elysia"; +import type { HTTPHeaders } from "elysia/dist/types"; +import type { ElysiaCookie } from "elysia/dist/cookies"; + +interface set { + headers: HTTPHeaders; + status?: number | keyof StatusMap; + redirect?: string; + cookie?: Record; +} + +export { set }; From c1113fc7056481a1ce32df7cc7105185f4cd063c Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Tue, 18 Mar 2025 11:39:24 +0000 Subject: [PATCH 190/324] Update dependency graphs --- dependency-graph.dot | 10 + dependency-graph.mmd | 250 +++++++------ dependency-graph.svg | 849 +++++++++++++++++++++++-------------------- 3 files changed, 601 insertions(+), 508 deletions(-) diff --git a/dependency-graph.dot b/dependency-graph.dot index 4cf81c6..9de730a 100644 --- a/dependency-graph.dot +++ b/dependency-graph.dot @@ -100,8 +100,11 @@ strict digraph "dependency-cruiser output"{ "src/core/utils/package-json.ts" -> "package.json" subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/respone-handler.ts" [label= tooltip="respone-handler.ts" URL="src/core/utils/respone-handler.ts" fillcolor="#ddfeff"] } } } "src/core/utils/respone-handler.ts" -> "src/core/utils/logger.ts" + "src/core/utils/respone-handler.ts" -> "src/typings/elysiajs.ts" [arrowhead="onormal" penwidth="1.0"] subgraph "cluster_src" {label="src" "src/index.ts" [label= tooltip="index.ts" URL="src/index.ts" fillcolor="#ddfeff"] } + "src/index.ts" -> "src/middleware/auth.ts" "src/index.ts" -> "src/routes/stacks.ts" + "src/index.ts" -> "src/typings/database.ts" "src/index.ts" -> "src/core/database/repository.ts" "src/index.ts" -> "src/core/docker/scheduler.ts" "src/index.ts" -> "src/core/plugins/loader.ts" @@ -112,11 +115,17 @@ strict digraph "dependency-cruiser output"{ "src/index.ts" -> "src/routes/docker-stats.ts" "src/index.ts" -> "src/routes/docker-websocket.ts" "src/index.ts" -> "src/routes/logs.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/middleware" {label="middleware" "src/middleware/auth.ts" [label= tooltip="auth.ts" URL="src/middleware/auth.ts" fillcolor="#ddfeff"] } } + "src/middleware/auth.ts" -> "src/core/database/repository.ts" + "src/middleware/auth.ts" -> "src/core/utils/logger.ts" + "src/middleware/auth.ts" -> "src/typings/database.ts" + "src/middleware/auth.ts" -> "src/typings/elysiajs.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/api-config.ts" [label= tooltip="api-config.ts" URL="src/routes/api-config.ts" fillcolor="#ddfeff"] } } "src/routes/api-config.ts" -> "src/core/database/repository.ts" "src/routes/api-config.ts" -> "src/core/utils/logger.ts" "src/routes/api-config.ts" -> "src/core/utils/package-json.ts" "src/routes/api-config.ts" -> "src/core/utils/respone-handler.ts" + "src/routes/api-config.ts" -> "src/middleware/auth.ts" "src/routes/api-config.ts" -> "src/typings/database.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/docker-manager.ts" [label= tooltip="docker-manager.ts" URL="src/routes/docker-manager.ts" fillcolor="#ddfeff"] } } "src/routes/docker-manager.ts" -> "src/core/database/repository.ts" @@ -151,6 +160,7 @@ strict digraph "dependency-cruiser output"{ subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/docker-compose.ts" [label= tooltip="docker-compose.ts" URL="src/typings/docker-compose.ts" fillcolor="#ddfeff"] } } subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/docker.ts" [label= tooltip="docker.ts" URL="src/typings/docker.ts" fillcolor="#ddfeff"] } } subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/dockerode.ts" [label= tooltip="dockerode.ts" URL="src/typings/dockerode.ts" fillcolor="#ddfeff"] } } + subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/elysiajs.ts" [label= tooltip="elysiajs.ts" URL="src/typings/elysiajs.ts" fillcolor="#ddfeff"] } } subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/plugin.ts" [label= tooltip="plugin.ts" URL="src/typings/plugin.ts" fillcolor="#ddfeff"] } } "src/typings/plugin.ts" -> "src/typings/docker.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/websocket.ts" [label= tooltip="websocket.ts" URL="src/typings/websocket.ts" fillcolor="#ddfeff"] } } diff --git a/dependency-graph.mmd b/dependency-graph.mmd index fe385f8..23ac0c9 100644 --- a/dependency-graph.mmd +++ b/dependency-graph.mmd @@ -8,13 +8,8 @@ flowchart LR subgraph 0["src"] 1["index.ts"] -subgraph 2["routes"] -3["stacks.ts"] -1A["api-config.ts"] -1B["docker-manager.ts"] -1C["docker-stats.ts"] -1D["docker-websocket.ts"] -1G["logs.ts"] +subgraph 2["middleware"] +3["auth.ts"] end subgraph 4["core"] subgraph 5["database"] @@ -23,69 +18,80 @@ subgraph 5["database"] end subgraph 9["utils"] A["logger.ts"] -I["respone-handler.ts"] -P["calculations.ts"] -T["change-me-checker.ts"] -14["package-json.ts"] +L["respone-handler.ts"] +S["calculations.ts"] +W["change-me-checker.ts"] +17["package-json.ts"] end -subgraph F["stacks"] -G["controller.ts"] +subgraph I["stacks"] +J["controller.ts"] end -subgraph J["docker"] -K["scheduler.ts"] -L["store-host-stats.ts"] -M["client.ts"] -O["store-container-stats.ts"] +subgraph M["docker"] +N["scheduler.ts"] +O["store-host-stats.ts"] +P["client.ts"] +R["store-container-stats.ts"] end -subgraph Q["plugins"] -R["loader.ts"] -V["plugin-manager.ts"] +subgraph T["plugins"] +U["loader.ts"] +Y["plugin-manager.ts"] end -subgraph Y["trpc"] -Z["index.ts"] -10["router.ts"] -subgraph 11["procedures"] -12["api-config.procedure.ts"] -16["docker-manager.procedure.ts"] -17["docker-stats.procedure.ts"] -18["logs.procedure.ts"] -19["stacks.procedure.ts"] +subgraph 11["trpc"] +12["index.ts"] +13["router.ts"] +subgraph 14["procedures"] +15["api-config.procedure.ts"] +19["docker-manager.procedure.ts"] +1A["docker-stats.procedure.ts"] +1B["logs.procedure.ts"] +1C["stacks.procedure.ts"] end -13["trpc.ts"] +16["trpc.ts"] end end subgraph C["typings"] D["database.ts"] E["docker.ts"] -H["docker-compose.ts"] -N["dockerode.ts"] -X["plugin.ts"] -1F["websocket.ts"] +F["elysiajs.ts"] +K["docker-compose.ts"] +Q["dockerode.ts"] +10["plugin.ts"] +1I["websocket.ts"] +end +subgraph G["routes"] +H["stacks.ts"] +1D["api-config.ts"] +1E["docker-manager.ts"] +1F["docker-stats.ts"] +1G["docker-websocket.ts"] +1J["logs.ts"] end end 7["bun:sqlite"] B["path"] -subgraph S["fs"] -U["promises"] +subgraph V["fs"] +X["promises"] end -W["events"] -15["package.json"] -1E["stream"] +Z["events"] +18["package.json"] +1H["stream"] 1-->3 +1-->H +1-->D 1-->6 -1-->K -1-->R -1-->Z +1-->N +1-->U +1-->12 1-->A -1-->1A -1-->1B -1-->1C 1-->1D +1-->1E +1-->1F 1-->1G +1-->1J 3-->6 -3-->G 3-->A -3-->I +3-->D +3-->F 6-->8 6-->A 6-->D @@ -94,92 +100,98 @@ W["events"] 8-->A A-->6 A-->B -G-->6 -G-->A -G-->D -G-->H -I-->A -K-->6 -K-->L -K-->O -K-->A -K-->D -L-->6 -L-->M +H-->6 +H-->J +H-->A +H-->L +J-->6 +J-->A +J-->D +J-->K L-->A -L-->E -L-->N -M-->A -M-->E +L-->F +N-->6 +N-->O +N-->R +N-->A +N-->D O-->6 -O-->M O-->P -R-->T -R-->A -R-->V +O-->A +O-->E +O-->Q +P-->A +P-->E +R-->6 +R-->P R-->S -R-->B -T-->A -T-->U -V-->A -V-->E -V-->X -V-->W -X-->E -Z-->10 -10-->12 -10-->16 -10-->17 -10-->18 -10-->19 -10-->13 +U-->W +U-->A +U-->Y +U-->V +U-->B +W-->A +W-->X +Y-->A +Y-->E +Y-->10 +Y-->Z +10-->E 12-->13 -12-->6 -12-->A -12-->14 -12-->D -14-->15 -16-->13 -16-->6 -16-->A -17-->13 -17-->6 -17-->M -17-->P -17-->A -17-->E -17-->N -18-->13 -18-->6 -18-->A -19-->13 +13-->15 +13-->19 +13-->1A +13-->1B +13-->1C +13-->16 +15-->16 +15-->6 +15-->A +15-->17 +15-->D +17-->18 +19-->16 19-->6 -19-->G 19-->A +1A-->16 1A-->6 +1A-->P +1A-->S 1A-->A -1A-->14 -1A-->I -1A-->D +1A-->E +1A-->Q +1B-->16 1B-->6 1B-->A -1B-->I +1C-->16 1C-->6 -1C-->M -1C-->P +1C-->J 1C-->A -1C-->I -1C-->E -1C-->N 1D-->6 -1D-->M -1D-->P 1D-->A -1D-->I -1D-->E -1D-->1F -1D-->1E -1F-->1E +1D-->17 +1D-->L +1D-->3 +1D-->D +1E-->6 +1E-->A +1E-->L +1F-->6 +1F-->P +1F-->S +1F-->A +1F-->L +1F-->E +1F-->Q 1G-->6 +1G-->P +1G-->S 1G-->A +1G-->L +1G-->E +1G-->1I +1G-->1H +1I-->1H +1J-->6 +1J-->A diff --git a/dependency-graph.svg b/dependency-graph.svg index ada00d6..dcc1098 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -4,11 +4,11 @@ - - + + dependency-cruiser output - + cluster_fs @@ -16,65 +16,70 @@ cluster_src - -src + +src cluster_src/core - -core + +core cluster_src/core/database - -database + +database cluster_src/core/docker - -docker + +docker cluster_src/core/plugins - -plugins + +plugins cluster_src/core/stacks - -stacks + +stacks cluster_src/core/trpc - -trpc + +trpc cluster_src/core/trpc/procedures - -procedures + +procedures cluster_src/core/utils - -utils + +utils -cluster_src/routes - -routes +cluster_src/middleware + +middleware +cluster_src/routes + +routes + + cluster_src/typings - -typings + +typings bun:sqlite - -bun:sqlite + +bun:sqlite @@ -109,8 +114,8 @@ package.json - -package.json + +package.json @@ -127,8 +132,8 @@ src/core/database/helper.ts - -helper.ts + +helper.ts @@ -136,394 +141,394 @@ src/core/utils/logger.ts - -logger.ts + +logger.ts src/core/database/helper.ts->src/core/utils/logger.ts - - - - + + + + src/core/utils/logger.ts->path - - + + src/core/database/repository.ts - -repository.ts + +repository.ts src/core/utils/logger.ts->src/core/database/repository.ts - - - - + + + + src/core/database/repository.ts->bun:sqlite - - + + src/core/database/repository.ts->src/core/database/helper.ts - - - - + + + + src/core/database/repository.ts->src/core/utils/logger.ts - - - - + + + + src/typings/database.ts - -database.ts + +database.ts src/core/database/repository.ts->src/typings/database.ts - - + + src/typings/docker.ts - -docker.ts + +docker.ts src/core/database/repository.ts->src/typings/docker.ts - - + + src/core/docker/client.ts - -client.ts + +client.ts src/core/docker/client.ts->src/core/utils/logger.ts - - + + src/core/docker/client.ts->src/typings/docker.ts - - + + src/core/docker/scheduler.ts - -scheduler.ts + +scheduler.ts src/core/docker/scheduler.ts->src/core/utils/logger.ts - - + + src/core/docker/scheduler.ts->src/core/database/repository.ts - - + + src/core/docker/scheduler.ts->src/typings/database.ts - - + + src/core/docker/store-host-stats.ts - -store-host-stats.ts + +store-host-stats.ts src/core/docker/scheduler.ts->src/core/docker/store-host-stats.ts - - + + src/core/docker/store-container-stats.ts - -store-container-stats.ts + +store-container-stats.ts src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts - - + + src/core/docker/store-host-stats.ts->src/core/utils/logger.ts - - + + src/core/docker/store-host-stats.ts->src/core/database/repository.ts - - + + src/core/docker/store-host-stats.ts->src/typings/docker.ts - - + + src/core/docker/store-host-stats.ts->src/core/docker/client.ts - - + + src/typings/dockerode.ts - -dockerode.ts + +dockerode.ts src/core/docker/store-host-stats.ts->src/typings/dockerode.ts - - + + src/core/docker/store-container-stats.ts->src/core/database/repository.ts - - + + src/core/docker/store-container-stats.ts->src/core/docker/client.ts - - + + src/core/utils/calculations.ts - -calculations.ts + +calculations.ts src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts - - + + src/core/plugins/loader.ts - -loader.ts + +loader.ts src/core/plugins/loader.ts->fs - - + + src/core/plugins/loader.ts->path - - + + src/core/plugins/loader.ts->src/core/utils/logger.ts - - + + src/core/utils/change-me-checker.ts - -change-me-checker.ts + +change-me-checker.ts src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts - - + + src/core/plugins/plugin-manager.ts - -plugin-manager.ts + +plugin-manager.ts src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts - - + + src/core/utils/change-me-checker.ts->fs/promises - + src/core/utils/change-me-checker.ts->src/core/utils/logger.ts - - + + src/core/plugins/plugin-manager.ts->events - + src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts - - + + src/core/plugins/plugin-manager.ts->src/typings/docker.ts - - + + src/typings/plugin.ts - -plugin.ts + +plugin.ts src/core/plugins/plugin-manager.ts->src/typings/plugin.ts - - + + - + src/typings/plugin.ts->src/typings/docker.ts - - + + src/core/stacks/controller.ts - -controller.ts + +controller.ts src/core/stacks/controller.ts->src/core/utils/logger.ts - - + + src/core/stacks/controller.ts->src/core/database/repository.ts - - + + src/core/stacks/controller.ts->src/typings/database.ts - - + + src/typings/docker-compose.ts - -docker-compose.ts + +docker-compose.ts src/core/stacks/controller.ts->src/typings/docker-compose.ts - - + + src/core/trpc/index.ts - -index.ts + +index.ts @@ -531,595 +536,661 @@ src/core/trpc/router.ts - -router.ts + +router.ts src/core/trpc/index.ts->src/core/trpc/router.ts - - + + src/core/trpc/procedures/api-config.procedure.ts - -api-config.procedure.ts + +api-config.procedure.ts src/core/trpc/router.ts->src/core/trpc/procedures/api-config.procedure.ts - - + + src/core/trpc/trpc.ts - -trpc.ts + +trpc.ts src/core/trpc/router.ts->src/core/trpc/trpc.ts - - + + src/core/trpc/procedures/docker-manager.procedure.ts - -docker-manager.procedure.ts + +docker-manager.procedure.ts src/core/trpc/router.ts->src/core/trpc/procedures/docker-manager.procedure.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts - -docker-stats.procedure.ts + +docker-stats.procedure.ts src/core/trpc/router.ts->src/core/trpc/procedures/docker-stats.procedure.ts - - + + src/core/trpc/procedures/logs.procedure.ts - -logs.procedure.ts + +logs.procedure.ts src/core/trpc/router.ts->src/core/trpc/procedures/logs.procedure.ts - - + + src/core/trpc/procedures/stacks.procedure.ts - -stacks.procedure.ts + +stacks.procedure.ts src/core/trpc/router.ts->src/core/trpc/procedures/stacks.procedure.ts - - + + src/core/trpc/procedures/api-config.procedure.ts->src/core/utils/logger.ts - - + + src/core/trpc/procedures/api-config.procedure.ts->src/core/database/repository.ts - - + + src/core/trpc/procedures/api-config.procedure.ts->src/typings/database.ts - - + + src/core/trpc/procedures/api-config.procedure.ts->src/core/trpc/trpc.ts - - + + src/core/utils/package-json.ts - -package-json.ts + +package-json.ts src/core/trpc/procedures/api-config.procedure.ts->src/core/utils/package-json.ts - - + + src/core/utils/package-json.ts->package.json - - + + src/core/trpc/procedures/docker-manager.procedure.ts->src/core/utils/logger.ts - - + + src/core/trpc/procedures/docker-manager.procedure.ts->src/core/database/repository.ts - - + + src/core/trpc/procedures/docker-manager.procedure.ts->src/core/trpc/trpc.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/utils/logger.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/database/repository.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/docker.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/docker/client.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/utils/calculations.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/dockerode.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/trpc/trpc.ts - - + + src/core/trpc/procedures/logs.procedure.ts->src/core/utils/logger.ts - - + + src/core/trpc/procedures/logs.procedure.ts->src/core/database/repository.ts - - + + src/core/trpc/procedures/logs.procedure.ts->src/core/trpc/trpc.ts - - + + src/core/trpc/procedures/stacks.procedure.ts->src/core/utils/logger.ts - - + + src/core/trpc/procedures/stacks.procedure.ts->src/core/database/repository.ts - - + + src/core/trpc/procedures/stacks.procedure.ts->src/core/stacks/controller.ts - - + + src/core/trpc/procedures/stacks.procedure.ts->src/core/trpc/trpc.ts - - + + src/core/utils/respone-handler.ts - -respone-handler.ts + +respone-handler.ts src/core/utils/respone-handler.ts->src/core/utils/logger.ts - - + + - + +src/typings/elysiajs.ts + + +elysiajs.ts + + + + + +src/core/utils/respone-handler.ts->src/typings/elysiajs.ts + + + + + src/index.ts - - -index.ts + + +index.ts - + src/index.ts->src/core/utils/logger.ts - - + + - + src/index.ts->src/core/database/repository.ts - - + + + + + +src/index.ts->src/typings/database.ts + + - + src/index.ts->src/core/docker/scheduler.ts - - + + - + src/index.ts->src/core/plugins/loader.ts - - + + - + src/index.ts->src/core/trpc/index.ts - - + + + + + +src/middleware/auth.ts + + +auth.ts + + + + + +src/index.ts->src/middleware/auth.ts + + - + src/routes/stacks.ts - - -stacks.ts + + +stacks.ts - + src/index.ts->src/routes/stacks.ts - - + + - + src/routes/api-config.ts - - -api-config.ts + + +api-config.ts - + src/index.ts->src/routes/api-config.ts - - + + - + src/routes/docker-manager.ts - - -docker-manager.ts + + +docker-manager.ts - + src/index.ts->src/routes/docker-manager.ts - - + + - + src/routes/docker-stats.ts - - -docker-stats.ts + + +docker-stats.ts - + src/index.ts->src/routes/docker-stats.ts - - + + - + src/routes/docker-websocket.ts - - -docker-websocket.ts + + +docker-websocket.ts - + src/index.ts->src/routes/docker-websocket.ts - - + + - + src/routes/logs.ts - - -logs.ts + + +logs.ts - + src/index.ts->src/routes/logs.ts - - + + + + + +src/middleware/auth.ts->src/core/utils/logger.ts + + + + + +src/middleware/auth.ts->src/core/database/repository.ts + + + + + +src/middleware/auth.ts->src/typings/database.ts + + + + + +src/middleware/auth.ts->src/typings/elysiajs.ts + + - + src/routes/stacks.ts->src/core/utils/logger.ts - - + + - + src/routes/stacks.ts->src/core/database/repository.ts - - + + - + src/routes/stacks.ts->src/core/stacks/controller.ts - - + + - + src/routes/stacks.ts->src/core/utils/respone-handler.ts - - + + - + src/routes/api-config.ts->src/core/utils/logger.ts - - + + - + src/routes/api-config.ts->src/core/database/repository.ts - - + + - + src/routes/api-config.ts->src/typings/database.ts - - + + - + src/routes/api-config.ts->src/core/utils/package-json.ts - - + + - + src/routes/api-config.ts->src/core/utils/respone-handler.ts - - + + + + + +src/routes/api-config.ts->src/middleware/auth.ts + + - + src/routes/docker-manager.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-manager.ts->src/core/database/repository.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/respone-handler.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-stats.ts->src/core/database/repository.ts - - + + - + src/routes/docker-stats.ts->src/typings/docker.ts - - + + - + src/routes/docker-stats.ts->src/core/docker/client.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-stats.ts->src/typings/dockerode.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/respone-handler.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-websocket.ts->src/core/database/repository.ts - - + + - + src/routes/docker-websocket.ts->src/typings/docker.ts - - + + - + src/routes/docker-websocket.ts->src/core/docker/client.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/respone-handler.ts - - + + - + src/typings/websocket.ts - - -websocket.ts + + +websocket.ts - + src/routes/docker-websocket.ts->src/typings/websocket.ts - - + + - + stream - + stream - + src/routes/docker-websocket.ts->stream - - + + - + src/routes/logs.ts->src/core/utils/logger.ts - - + + - + src/routes/logs.ts->src/core/database/repository.ts - - + + - + src/typings/websocket.ts->stream - - + + From df63924b7d12ebe793c51f1435e1a7a9f15a8505 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 18 Mar 2025 22:33:48 +0100 Subject: [PATCH 191/324] Feat: Plugins with their respectful hooks implemented. Fixes #42 --- .local-tests/test-container-changes.sh | 15 +++ src/core/docker/monitor.ts | 136 +++++++++++++++++++++++++ src/core/plugins/plugin-actions.ts | 7 -- src/core/plugins/plugin-manager.ts | 23 ++++- src/core/utils/calculations.ts | 7 ++ src/core/utils/logger.ts | 34 ++++--- src/index.ts | 8 +- src/plugins/example.plugin.ts | 103 ++++++++++++++++--- src/typings/plugin.ts | 6 +- 9 files changed, 296 insertions(+), 43 deletions(-) create mode 100644 .local-tests/test-container-changes.sh create mode 100644 src/core/docker/monitor.ts delete mode 100644 src/core/plugins/plugin-actions.ts diff --git a/.local-tests/test-container-changes.sh b/.local-tests/test-container-changes.sh new file mode 100644 index 0000000..5df5075 --- /dev/null +++ b/.local-tests/test-container-changes.sh @@ -0,0 +1,15 @@ +commands=("kill" "start" "restart" "start" "pause" "unpause") +container="SQLite-web" + +press(){ + echo "Press enter to continue" + read -r -p ">" +} + +for command in "${commands[@]}"; do + press + echo "Running $command for $container" + docker "$command" "$container" +done + +docker start "$container" diff --git a/src/core/docker/monitor.ts b/src/core/docker/monitor.ts new file mode 100644 index 0000000..fe8df25 --- /dev/null +++ b/src/core/docker/monitor.ts @@ -0,0 +1,136 @@ +import type { DockerHost } from "~/typings/docker"; +import { dbFunctions } from "~/core/database/repository"; +import { getDockerClient } from "~/core/docker/client"; +import { logger } from "~/core/utils/logger"; +import { pluginManager } from "../plugins/plugin-manager"; +import { HostStats, ContainerInfo } from "~/typings/docker"; +import { sleep } from "bun"; + +export async function monitorDockerEvents() { + let hosts: DockerHost[]; + + try { + hosts = dbFunctions.getDockerHosts(); + logger.debug( + `Retrieved ${hosts.length} Docker host(s) for event monitoring.`, + ); + } catch (error: unknown) { + logger.error(`Error retrieving Docker hosts: ${(error as Error).message}`); + return; + } + + for (const host of hosts) { + await startFor(host); + } +} + +async function startFor(host: DockerHost) { + const docker = getDockerClient(host); + try { + await docker.ping(); + pluginManager.handleHostReachableAgain(host.name); + } catch (err: any) { + logger.warn(`Restarting Stream for ${host.name} in 10 seconds...`); + pluginManager.handleHostUnreachable(host.name, err); + await sleep(10000); + startFor(host); + } + + try { + const eventsStream = await docker.getEvents(); + logger.debug(`Started events stream for host: ${host.name}`); + + let buffer = ""; + + eventsStream.on("data", (chunk: Buffer) => { + buffer += chunk.toString("utf8"); + const lines = buffer.split(/\r?\n/); + + buffer = lines.pop() || ""; + + for (const line of lines) { + if (line.trim() === "") continue; + + let event: any; + try { + event = JSON.parse(line); + } catch (parseErr: any) { + logger.error( + `Failed to parse event from host ${host.name}: ${parseErr.message}`, + ); + continue; + } + + if (event.Type === "container") { + const containerInfo: ContainerInfo = { + id: event.Actor?.ID || event.id || "", + hostId: host.name, + name: event.Actor?.Attributes?.name || "", + image: event.Actor?.Attributes?.image || event.from || "", + status: event.status || event.Actor?.Attributes?.status || "", + state: event.Actor?.Attributes?.state || event.Action || "", + cpuUsage: 0, + memoryUsage: 0, + }; + + const action = event.Action; + logger.debug(`Triggering Action [${action}]`); + switch (action) { + case "stop": + pluginManager.handleContainerStop(containerInfo); + break; + case "start": + pluginManager.handleContainerStart(containerInfo); + break; + case "die": + pluginManager.handleContainerDie(containerInfo); + break; + case "kill": + pluginManager.handleContainerKill(containerInfo); + break; + case "create": + pluginManager.handleContainerCreate(containerInfo); + break; + case "destroy": + pluginManager.handleContainerDestroy(containerInfo); + break; + case "pause": + pluginManager.handleContainerPause(containerInfo); + break; + case "unpause": + pluginManager.handleContainerUnpause(containerInfo); + break; + case "restart": + pluginManager.handleContainerRestart(containerInfo); + break; + case "update": + pluginManager.handleContainerUpdate(containerInfo); + break; + case "health_status": + pluginManager.handleContainerHealthStatus(containerInfo); + break; + default: + logger.debug( + `Unhandled container event "${action}" on host ${host.name}`, + ); + } + } + } + }); + + eventsStream.on("error", async (err: Error) => { + logger.error(`Events stream error for host ${host.name}: ${err.message}`); + logger.warn(`Restarting Stream for ${host.name} in 10 seconds...`); + await sleep(10000); + startFor(host); + }); + + eventsStream.on("end", () => { + logger.info(`Events stream ended for host ${host.name}`); + }); + } catch (streamErr: any) { + logger.error( + `Failed to start events stream for host ${host.name}: ${streamErr.message}`, + ); + } +} diff --git a/src/core/plugins/plugin-actions.ts b/src/core/plugins/plugin-actions.ts deleted file mode 100644 index f914681..0000000 --- a/src/core/plugins/plugin-actions.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { pluginManager } from "./plugin-manager"; - -export const pluginAction = { - containerStart(containerInfo: any) { - pluginManager.handleContainerStart(containerInfo); - }, -}; diff --git a/src/core/plugins/plugin-manager.ts b/src/core/plugins/plugin-manager.ts index a81aa0e..c030b09 100644 --- a/src/core/plugins/plugin-manager.ts +++ b/src/core/plugins/plugin-manager.ts @@ -2,6 +2,7 @@ import { EventEmitter } from "events"; import { logger } from "../utils/logger"; import type { Plugin } from "~/typings/plugin"; import type { ContainerInfo, HostStats } from "~/typings/docker"; +import { plugin } from "bun"; export class PluginManager extends EventEmitter { private plugins: Map = new Map(); @@ -12,7 +13,7 @@ export class PluginManager extends EventEmitter { logger.debug(`Registered plugin: ${plugin.name}`); } catch (error) { logger.error( - `Registering plugin ${plugin.name} failed: ${error as string}` + `Registering plugin ${plugin.name} failed: ${error as string}`, ); } } @@ -88,15 +89,27 @@ export class PluginManager extends EventEmitter { }); } - handleHostUnreachable(HostStats: HostStats) { + handleHostUnreachable(host: string, err: string) { this.plugins.forEach((plugin) => { - plugin.onHostUnreachable?.(HostStats); + plugin.onHostUnreachable?.(host, err); }); } - handleHostReachableAgain(HostStats: HostStats) { + handleHostReachableAgain(host: string) { this.plugins.forEach((plugin) => { - plugin.onHostReachableAgain?.(HostStats); + plugin.onHostReachableAgain?.(host); + }); + } + + handleContainerKill(containerInfo: ContainerInfo) { + this.plugins.forEach((plugin) => { + plugin.onContainerKill?.(containerInfo); + }); + } + + handleContainerDie(containerInfo: ContainerInfo) { + this.plugins.forEach((plugin) => { + plugin.handleContainerDie?.(containerInfo); }); } } diff --git a/src/core/utils/calculations.ts b/src/core/utils/calculations.ts index 3ead3a6..3d7a81d 100644 --- a/src/core/utils/calculations.ts +++ b/src/core/utils/calculations.ts @@ -1,6 +1,10 @@ import type Docker from "dockerode"; const calculateCpuPercent = (stats: Docker.ContainerStats): number => { + if (stats == null) { + return 0.0; + } + const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - stats.precpu_stats.cpu_usage.total_usage; @@ -10,6 +14,9 @@ const calculateCpuPercent = (stats: Docker.ContainerStats): number => { }; const calculateMemoryUsage = (stats: Docker.ContainerStats): number => { + if (stats == null) { + return 0.0; + } return (stats.memory_stats.usage / stats.memory_stats.limit) * 100; }; diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts index b35aeea..8c321ad 100644 --- a/src/core/utils/logger.ts +++ b/src/core/utils/logger.ts @@ -14,7 +14,10 @@ const fileLineFormat = format((info) => { for (let i = 2; i < stack.length; i++) { const line = stack[i].trim(); // Exclude lines from node_modules or the current file - if (!line.includes("node_modules") && !line.includes(path.basename(__filename))) { + if ( + !line.includes("node_modules") && + !line.includes(path.basename(__filename)) + ) { const matches = line.match(/\(?(.+):(\d+):(\d+)\)?$/); if (matches) { info.file = path.basename(matches[1]); @@ -49,12 +52,11 @@ const formatTerminalMessage = (message: string, prefixLength: number) => { }; export const logger = createLogger({ - level: process.env.LOG_LEVEL || 'debug', + level: process.env.LOG_LEVEL || "debug", format: format.combine( format.timestamp({ format: "DD/MM HH:mm:ss" }), fileLineFormat(), format.printf(({ timestamp, level, message, file, line }) => { - const levelColors: Record = { error: chalk.red.bold, warn: chalk.yellow.bold, @@ -62,18 +64,22 @@ export const logger = createLogger({ debug: chalk.blue.bold, verbose: chalk.cyan.bold, silly: chalk.magenta.bold, - task: chalk.cyan.bold + task: chalk.cyan.bold, }; if ((message as string).startsWith("__task__")) { message = (message as string).replaceAll("__task__", "").trimStart(); - level = "task" + level = "task"; if ((message as string).startsWith("__db__")) { message = (message as string).replaceAll("__db__", "").trimStart(); - message = `${chalk.magenta("DB")} ${message}` + message = `${chalk.magenta("DB")} ${message}`; } } + if ((file as string).includes("plugin.ts")) { + message = `[ ${chalk.greenBright("Plugin")} ] ${message}`; + } + const paddedLevel = level.toUpperCase().padEnd(5); const coloredLevel = (levelColors[level] || chalk.white)(paddedLevel); const coloredContext = chalk.cyan(`${file as string}:${line as number}`); @@ -81,7 +87,7 @@ export const logger = createLogger({ if (process.env.NODE_ENV !== "dev") { return `${coloredLevel} [ ${coloredTimestamp} ] - ${chalk.gray( - message + message, )} - [ ${coloredContext} ]`; } @@ -89,25 +95,25 @@ export const logger = createLogger({ const prefixLength = prefix.length; const formattedMessage = formatTerminalMessage( message as string, - prefixLength + prefixLength, ); const ansiRegex = /\x1B\[[0-?9;]*[mG]/g; try { dbFunctions.addLogEntry( - (level as string).replace(ansiRegex, ''), - (message as string).replace(ansiRegex, ''), - (file as string).replace(ansiRegex, ''), - line as number + (level as string).replace(ansiRegex, ""), + (message as string).replace(ansiRegex, ""), + (file as string).replace(ansiRegex, ""), + line as number, ); } catch (error) { // Use console.error to avoid recursive logging console.error(`Error inserting log into DB: ${String(error)}`); - process.abort() + process.abort(); } return `${coloredLevel} [ ${coloredTimestamp} ] - ${formattedMessage} - [ ${coloredContext} ]`; - }) + }), ), transports: [new transports.Console()], }); diff --git a/src/index.ts b/src/index.ts index 7aa4702..ff6b763 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,6 +15,7 @@ import staticPlugin from "@elysiajs/static"; import trpcRouter from "~/core/trpc"; import { config } from "./typings/database"; import { validateApiKey } from "./middleware/auth"; +import { monitorDockerEvents } from "./core/docker/monitor"; console.log(""); dbFunctions.init(); @@ -103,6 +104,12 @@ const DockStatAPI = new Elysia() async function startServer() { try { await loadPlugins("./src/plugins"); + await setSchedules(); + + monitorDockerEvents().catch((error) => { + logger.error(`Monitoring Error: ${error}`); + }); + const configData = dbFunctions.getConfig() as config[]; const apiKey = configData[0].api_key; @@ -128,7 +135,6 @@ async function startServer() { } } -await setSchedules(); await startServer(); logger.info("Started server"); diff --git a/src/plugins/example.plugin.ts b/src/plugins/example.plugin.ts index bd71bec..d1a1ec6 100644 --- a/src/plugins/example.plugin.ts +++ b/src/plugins/example.plugin.ts @@ -1,22 +1,97 @@ import type { Plugin } from "~/typings/plugin"; import type { ContainerInfo } from "~/typings/docker"; -import type { HostStats } from "~/typings/docker"; +import { logger } from "~/core/utils/logger"; + +// See https://outline.itsnik.de/s/dockstat/doc/plugin-development-3UBj9gNMKF for more info const ExamplePlugin: Plugin = { name: "Example Plugin", - async onContainerStart(containerInfo: ContainerInfo) {}, - async onContainerStop(containerInfo: ContainerInfo) {}, - async onContainerExit(containerInfo: ContainerInfo) {}, - async onContainerCreate(containerInfo: ContainerInfo) {}, - async onContainerDestroy(containerInfo: ContainerInfo) {}, - async onContainerPause(containerInfo: ContainerInfo) {}, - async onContainerUnpause(containerInfo: ContainerInfo) {}, - async onContainerRestart(containerInfo: ContainerInfo) {}, - async onContainerUpdate(containerInfo: ContainerInfo) {}, - async onContainerRename(containerInfo: ContainerInfo) {}, - async onContainerHealthStatus(containerInfo: ContainerInfo) {}, - async onHostUnreachable(HostStats: HostStats) {}, - async onHostReachableAgain(HostStats: HostStats) {}, + + async onContainerStart(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} started on ${containerInfo.hostId}`, + ); + }, + + async onContainerStop(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} stopped on ${containerInfo.hostId}`, + ); + }, + + async onContainerExit(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} exited on ${containerInfo.hostId}`, + ); + }, + + async onContainerCreate(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} created on ${containerInfo.hostId}`, + ); + }, + + async onContainerDestroy(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} destroyed on ${containerInfo.hostId}`, + ); + }, + + async onContainerPause(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} pause on ${containerInfo.hostId}`, + ); + }, + + async onContainerUnpause(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} resumed on ${containerInfo.hostId}`, + ); + }, + + async onContainerRestart(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} restarted on ${containerInfo.hostId}`, + ); + }, + + async onContainerUpdate(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} updated on ${containerInfo.hostId}`, + ); + }, + + async onContainerRename(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} renamed on ${containerInfo.hostId}`, + ); + }, + + async onContainerHealthStatus(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} changed status to ${containerInfo.status}`, + ); + }, + + async onHostUnreachable(host: string, err: string) { + logger.info(`Server ${host} unreachable - ${err}`); + }, + + async onHostReachableAgain(host: string) { + logger.info(`Server ${host} reachable`); + }, + + async handleContainerDie(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} died on ${containerInfo.hostId}`, + ); + }, + + async onContainerKill(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} killed on ${containerInfo.hostId}`, + ); + }, } satisfies Plugin; export default ExamplePlugin; diff --git a/src/typings/plugin.ts b/src/typings/plugin.ts index 9994ea6..ee16559 100644 --- a/src/typings/plugin.ts +++ b/src/typings/plugin.ts @@ -9,6 +9,8 @@ interface Plugin { onContainerStop?: (containerInfo: ContainerInfo) => void; onContainerExit?: (containerInfo: ContainerInfo) => void; onContainerCreate?: (containerInfo: ContainerInfo) => void; + onContainerKill?: (ContainerInfo: ContainerInfo) => void; + handleContainerDie?: (ContainerInfo: ContainerInfo) => void; onContainerDestroy?: (containerInfo: ContainerInfo) => void; onContainerPause?: (containerInfo: ContainerInfo) => void; onContainerUnpause?: (containerInfo: ContainerInfo) => void; @@ -18,8 +20,8 @@ interface Plugin { onContainerHealthStatus?: (containerInfo: ContainerInfo) => void; // Host lifecycle hooks - onHostUnreachable?: (HostStats: HostStats) => void; - onHostReachableAgain?: (HostStats: HostStats) => void; + onHostUnreachable?: (host: string, err: string) => void; + onHostReachableAgain?: (host: string) => void; } export type { Plugin }; From 98854042f724b4a12f3005f30a71c0afed7fc0f1 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Tue, 18 Mar 2025 21:35:31 +0000 Subject: [PATCH 192/324] Update dependency graphs --- dependency-graph.dot | 11 + dependency-graph.mmd | 315 +++++------ dependency-graph.svg | 1242 ++++++++++++++++++++++-------------------- 3 files changed, 828 insertions(+), 740 deletions(-) diff --git a/dependency-graph.dot b/dependency-graph.dot index 9de730a..06b5870 100644 --- a/dependency-graph.dot +++ b/dependency-graph.dot @@ -3,6 +3,7 @@ strict digraph "dependency-cruiser output"{ node [shape="box" style="rounded, filled" height="0.2" color="black" fillcolor="#ffffcc" fontcolor="black" fontname="Helvetica" fontsize="9"] edge [arrowhead="normal" arrowsize="0.6" penwidth="2.0" color="#00000033" fontname="Helvetica" fontsize="9"] + "bun" [label= tooltip="bun" ] "bun:sqlite" [label= tooltip="bun:sqlite" ] "events" [label= tooltip="events" URL="https://nodejs.org/api/events.html" color="grey" fontcolor="grey"] "fs" [label= tooltip="fs" URL="https://nodejs.org/api/fs.html" color="grey" fontcolor="grey"] @@ -20,6 +21,14 @@ strict digraph "dependency-cruiser output"{ subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/client.ts" [label= tooltip="client.ts" URL="src/core/docker/client.ts" fillcolor="#ddfeff"] } } } "src/core/docker/client.ts" -> "src/core/utils/logger.ts" "src/core/docker/client.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/monitor.ts" [label= tooltip="monitor.ts" URL="src/core/docker/monitor.ts" fillcolor="#ddfeff"] } } } + "src/core/docker/monitor.ts" -> "src/core/plugins/plugin-manager.ts" + "src/core/docker/monitor.ts" -> "src/core/database/repository.ts" + "src/core/docker/monitor.ts" -> "src/core/docker/client.ts" + "src/core/docker/monitor.ts" -> "src/core/utils/logger.ts" + "src/core/docker/monitor.ts" -> "src/typings/docker.ts" + "src/core/docker/monitor.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] + "src/core/docker/monitor.ts" -> "bun" subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/scheduler.ts" [label= tooltip="scheduler.ts" URL="src/core/docker/scheduler.ts" fillcolor="#ddfeff"] } } } "src/core/docker/scheduler.ts" -> "src/core/database/repository.ts" "src/core/docker/scheduler.ts" -> "src/core/docker/store-host-stats.ts" @@ -46,6 +55,7 @@ strict digraph "dependency-cruiser output"{ "src/core/plugins/plugin-manager.ts" -> "src/core/utils/logger.ts" "src/core/plugins/plugin-manager.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] "src/core/plugins/plugin-manager.ts" -> "src/typings/plugin.ts" [arrowhead="onormal" penwidth="1.0"] + "src/core/plugins/plugin-manager.ts" -> "bun" "src/core/plugins/plugin-manager.ts" -> "events" [style="dashed" penwidth="1.0"] subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/stacks" {label="stacks" "src/core/stacks/controller.ts" [label= tooltip="controller.ts" URL="src/core/stacks/controller.ts" fillcolor="#ddfeff"] } } } "src/core/stacks/controller.ts" -> "src/core/database/repository.ts" @@ -102,6 +112,7 @@ strict digraph "dependency-cruiser output"{ "src/core/utils/respone-handler.ts" -> "src/core/utils/logger.ts" "src/core/utils/respone-handler.ts" -> "src/typings/elysiajs.ts" [arrowhead="onormal" penwidth="1.0"] subgraph "cluster_src" {label="src" "src/index.ts" [label= tooltip="index.ts" URL="src/index.ts" fillcolor="#ddfeff"] } + "src/index.ts" -> "src/core/docker/monitor.ts" "src/index.ts" -> "src/middleware/auth.ts" "src/index.ts" -> "src/routes/stacks.ts" "src/index.ts" -> "src/typings/database.ts" diff --git a/dependency-graph.mmd b/dependency-graph.mmd index 23ac0c9..36b5db9 100644 --- a/dependency-graph.mmd +++ b/dependency-graph.mmd @@ -8,190 +8,201 @@ flowchart LR subgraph 0["src"] 1["index.ts"] -subgraph 2["middleware"] -3["auth.ts"] +subgraph 2["core"] +subgraph 3["docker"] +4["monitor.ts"] +K["client.ts"] +U["scheduler.ts"] +V["store-host-stats.ts"] +X["store-container-stats.ts"] end -subgraph 4["core"] -subgraph 5["database"] -6["repository.ts"] -8["helper.ts"] +subgraph 6["plugins"] +7["plugin-manager.ts"] +Z["loader.ts"] end subgraph 9["utils"] A["logger.ts"] -L["respone-handler.ts"] -S["calculations.ts"] -W["change-me-checker.ts"] -17["package-json.ts"] +T["respone-handler.ts"] +Y["calculations.ts"] +11["change-me-checker.ts"] +19["package-json.ts"] end -subgraph I["stacks"] -J["controller.ts"] +subgraph C["database"] +D["repository.ts"] +F["helper.ts"] end -subgraph M["docker"] -N["scheduler.ts"] -O["store-host-stats.ts"] -P["client.ts"] -R["store-container-stats.ts"] +subgraph Q["stacks"] +R["controller.ts"] end -subgraph T["plugins"] -U["loader.ts"] -Y["plugin-manager.ts"] +subgraph 13["trpc"] +14["index.ts"] +15["router.ts"] +subgraph 16["procedures"] +17["api-config.procedure.ts"] +1B["docker-manager.procedure.ts"] +1C["docker-stats.procedure.ts"] +1D["logs.procedure.ts"] +1E["stacks.procedure.ts"] end -subgraph 11["trpc"] -12["index.ts"] -13["router.ts"] -subgraph 14["procedures"] -15["api-config.procedure.ts"] -19["docker-manager.procedure.ts"] -1A["docker-stats.procedure.ts"] -1B["logs.procedure.ts"] -1C["stacks.procedure.ts"] +18["trpc.ts"] end -16["trpc.ts"] end +subgraph G["typings"] +H["database.ts"] +I["docker.ts"] +J["plugin.ts"] +N["elysiajs.ts"] +S["docker-compose.ts"] +W["dockerode.ts"] +1K["websocket.ts"] end -subgraph C["typings"] -D["database.ts"] -E["docker.ts"] -F["elysiajs.ts"] -K["docker-compose.ts"] -Q["dockerode.ts"] -10["plugin.ts"] -1I["websocket.ts"] +subgraph L["middleware"] +M["auth.ts"] end -subgraph G["routes"] -H["stacks.ts"] -1D["api-config.ts"] -1E["docker-manager.ts"] -1F["docker-stats.ts"] -1G["docker-websocket.ts"] -1J["logs.ts"] +subgraph O["routes"] +P["stacks.ts"] +1F["api-config.ts"] +1G["docker-manager.ts"] +1H["docker-stats.ts"] +1I["docker-websocket.ts"] +1L["logs.ts"] end end -7["bun:sqlite"] +5["bun"] +8["events"] B["path"] -subgraph V["fs"] -X["promises"] +E["bun:sqlite"] +subgraph 10["fs"] +12["promises"] end -Z["events"] -18["package.json"] -1H["stream"] -1-->3 +1A["package.json"] +1J["stream"] +1-->4 +1-->M +1-->P 1-->H 1-->D -1-->6 -1-->N 1-->U -1-->12 +1-->Z +1-->14 1-->A -1-->1D -1-->1E 1-->1F 1-->1G -1-->1J -3-->6 -3-->A -3-->D -3-->F -6-->8 -6-->A -6-->D -6-->E -6-->7 -8-->A -A-->6 +1-->1H +1-->1I +1-->1L +4-->7 +4-->D +4-->K +4-->A +4-->I +4-->I +4-->5 +7-->A +7-->I +7-->J +7-->5 +7-->8 +A-->D A-->B -H-->6 -H-->J -H-->A -H-->L -J-->6 -J-->A -J-->D -J-->K -L-->A -L-->F -N-->6 -N-->O -N-->R -N-->A -N-->D -O-->6 -O-->P -O-->A -O-->E -O-->Q +D-->F +D-->A +D-->H +D-->I +D-->E +F-->A +J-->I +K-->A +K-->I +M-->D +M-->A +M-->H +M-->N +P-->D +P-->R P-->A -P-->E -R-->6 -R-->P +P-->T +R-->D +R-->A +R-->H R-->S -U-->W -U-->A -U-->Y +T-->A +T-->N +U-->D U-->V -U-->B -W-->A -W-->X -Y-->A -Y-->E -Y-->10 -Y-->Z -10-->E -12-->13 -13-->15 -13-->19 -13-->1A -13-->1B -13-->1C -13-->16 -15-->16 -15-->6 -15-->A +U-->X +U-->A +U-->H +V-->D +V-->K +V-->A +V-->I +V-->W +X-->D +X-->K +X-->Y +Z-->11 +Z-->A +Z-->7 +Z-->10 +Z-->B +11-->A +11-->12 +14-->15 15-->17 -15-->D +15-->1B +15-->1C +15-->1D +15-->1E +15-->18 17-->18 -19-->16 -19-->6 -19-->A -1A-->16 -1A-->6 -1A-->P -1A-->S -1A-->A -1A-->E -1A-->Q -1B-->16 -1B-->6 +17-->D +17-->A +17-->19 +17-->H +19-->1A +1B-->18 +1B-->D 1B-->A -1C-->16 -1C-->6 -1C-->J +1C-->18 +1C-->D +1C-->K +1C-->Y 1C-->A -1D-->6 -1D-->A -1D-->17 -1D-->L -1D-->3 +1C-->I +1C-->W +1D-->18 1D-->D -1E-->6 +1D-->A +1E-->18 +1E-->D +1E-->R 1E-->A -1E-->L -1F-->6 -1F-->P -1F-->S +1F-->D 1F-->A -1F-->L -1F-->E -1F-->Q -1G-->6 -1G-->P -1G-->S +1F-->19 +1F-->T +1F-->M +1F-->H +1G-->D 1G-->A -1G-->L -1G-->E -1G-->1I -1G-->1H -1I-->1H -1J-->6 -1J-->A +1G-->T +1H-->D +1H-->K +1H-->Y +1H-->A +1H-->T +1H-->I +1H-->W +1I-->D +1I-->K +1I-->Y +1I-->A +1I-->T +1I-->I +1I-->1K +1I-->1J +1K-->1J +1L-->D +1L-->A diff --git a/dependency-graph.svg b/dependency-graph.svg index dcc1098..1537421 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -4,1193 +4,1259 @@ - - + + dependency-cruiser output - + cluster_fs - -fs + +fs cluster_src - -src + +src cluster_src/core - -core + +core cluster_src/core/database - -database + +database cluster_src/core/docker - -docker + +docker cluster_src/core/plugins - -plugins + +plugins cluster_src/core/stacks - -stacks + +stacks cluster_src/core/trpc - -trpc + +trpc cluster_src/core/trpc/procedures - -procedures + +procedures cluster_src/core/utils - -utils + +utils cluster_src/middleware - -middleware + +middleware cluster_src/routes - -routes + +routes cluster_src/typings - -typings + +typings - + +bun + + +bun + + + + + bun:sqlite - - -bun:sqlite + + +bun:sqlite - + events - - -events + + +events - + fs - - -fs + + +fs - + fs/promises - - -promises + + +promises - + package.json - - -package.json + + +package.json - + path - - -path + + +path - + src/core/database/helper.ts - - -helper.ts + + +helper.ts - + src/core/utils/logger.ts - - -logger.ts + + +logger.ts src/core/database/helper.ts->src/core/utils/logger.ts - - - - + + + + - + src/core/utils/logger.ts->path - - + + - + src/core/database/repository.ts - - -repository.ts + + +repository.ts - + src/core/utils/logger.ts->src/core/database/repository.ts - - - - + + + + src/core/database/repository.ts->bun:sqlite - - + + src/core/database/repository.ts->src/core/database/helper.ts - - - - + + + + src/core/database/repository.ts->src/core/utils/logger.ts - - - - + + + + - + src/typings/database.ts - - -database.ts + + +database.ts src/core/database/repository.ts->src/typings/database.ts - - + + - + src/typings/docker.ts - - -docker.ts + + +docker.ts src/core/database/repository.ts->src/typings/docker.ts - - + + - + src/core/docker/client.ts - - -client.ts + + +client.ts src/core/docker/client.ts->src/core/utils/logger.ts - - + + src/core/docker/client.ts->src/typings/docker.ts - - + + + + + +src/core/docker/monitor.ts + + +monitor.ts + + + + + +src/core/docker/monitor.ts->bun + + + + + +src/core/docker/monitor.ts->src/core/utils/logger.ts + + + + + +src/core/docker/monitor.ts->src/core/database/repository.ts + + + + + +src/core/docker/monitor.ts->src/typings/docker.ts + + + + + +src/core/docker/monitor.ts->src/core/docker/client.ts + + + + + +src/core/plugins/plugin-manager.ts + + +plugin-manager.ts + + + + + +src/core/docker/monitor.ts->src/core/plugins/plugin-manager.ts + + + + + +src/core/plugins/plugin-manager.ts->bun + + + + + +src/core/plugins/plugin-manager.ts->events + + + + + +src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts + + + + + +src/core/plugins/plugin-manager.ts->src/typings/docker.ts + + + + + +src/typings/plugin.ts + + +plugin.ts + + + + + +src/core/plugins/plugin-manager.ts->src/typings/plugin.ts + + - + src/core/docker/scheduler.ts - - -scheduler.ts + + +scheduler.ts - + src/core/docker/scheduler.ts->src/core/utils/logger.ts - - + + - + src/core/docker/scheduler.ts->src/core/database/repository.ts - - + + - + src/core/docker/scheduler.ts->src/typings/database.ts - - + + - + src/core/docker/store-host-stats.ts - - -store-host-stats.ts + + +store-host-stats.ts - + src/core/docker/scheduler.ts->src/core/docker/store-host-stats.ts - - + + - + src/core/docker/store-container-stats.ts - - -store-container-stats.ts + + +store-container-stats.ts - + src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/utils/logger.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/database/repository.ts - - + + - + src/core/docker/store-host-stats.ts->src/typings/docker.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/docker/client.ts - - + + - + src/typings/dockerode.ts - - -dockerode.ts + + +dockerode.ts - + src/core/docker/store-host-stats.ts->src/typings/dockerode.ts - - + + - + src/core/docker/store-container-stats.ts->src/core/database/repository.ts - - + + - + src/core/docker/store-container-stats.ts->src/core/docker/client.ts - - + + - + src/core/utils/calculations.ts - - -calculations.ts + + +calculations.ts - + src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts - - + + - + src/core/plugins/loader.ts - - -loader.ts + + +loader.ts - + src/core/plugins/loader.ts->fs - - + + - + src/core/plugins/loader.ts->path - - + + - + src/core/plugins/loader.ts->src/core/utils/logger.ts - - + + + + + +src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts + + - + src/core/utils/change-me-checker.ts - - -change-me-checker.ts + + +change-me-checker.ts - + src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts - - - - - -src/core/plugins/plugin-manager.ts - - -plugin-manager.ts - - - - - -src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts - - + + - + src/core/utils/change-me-checker.ts->fs/promises - - + + - + src/core/utils/change-me-checker.ts->src/core/utils/logger.ts - - - - - -src/core/plugins/plugin-manager.ts->events - - - - - -src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts - - - - - -src/core/plugins/plugin-manager.ts->src/typings/docker.ts - - - - - -src/typings/plugin.ts - - -plugin.ts - - - - - -src/core/plugins/plugin-manager.ts->src/typings/plugin.ts - - + + - + src/typings/plugin.ts->src/typings/docker.ts - - + + - + src/core/stacks/controller.ts - - -controller.ts + + +controller.ts - + src/core/stacks/controller.ts->src/core/utils/logger.ts - - + + - + src/core/stacks/controller.ts->src/core/database/repository.ts - - + + - + src/core/stacks/controller.ts->src/typings/database.ts - - + + - + src/typings/docker-compose.ts - - -docker-compose.ts + + +docker-compose.ts - + src/core/stacks/controller.ts->src/typings/docker-compose.ts - - + + - + src/core/trpc/index.ts - - -index.ts + + +index.ts - + src/core/trpc/router.ts - - -router.ts + + +router.ts - + src/core/trpc/index.ts->src/core/trpc/router.ts - - + + - + src/core/trpc/procedures/api-config.procedure.ts - - -api-config.procedure.ts + + +api-config.procedure.ts - + src/core/trpc/router.ts->src/core/trpc/procedures/api-config.procedure.ts - - + + - + src/core/trpc/trpc.ts - - -trpc.ts + + +trpc.ts - + src/core/trpc/router.ts->src/core/trpc/trpc.ts - - + + - + src/core/trpc/procedures/docker-manager.procedure.ts - - -docker-manager.procedure.ts + + +docker-manager.procedure.ts - + src/core/trpc/router.ts->src/core/trpc/procedures/docker-manager.procedure.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts - - -docker-stats.procedure.ts + + +docker-stats.procedure.ts - + src/core/trpc/router.ts->src/core/trpc/procedures/docker-stats.procedure.ts - - + + - + src/core/trpc/procedures/logs.procedure.ts - - -logs.procedure.ts + + +logs.procedure.ts - + src/core/trpc/router.ts->src/core/trpc/procedures/logs.procedure.ts - - + + - + src/core/trpc/procedures/stacks.procedure.ts - - -stacks.procedure.ts + + +stacks.procedure.ts - + src/core/trpc/router.ts->src/core/trpc/procedures/stacks.procedure.ts - - + + - + src/core/trpc/procedures/api-config.procedure.ts->src/core/utils/logger.ts - - + + - + src/core/trpc/procedures/api-config.procedure.ts->src/core/database/repository.ts - - + + - + src/core/trpc/procedures/api-config.procedure.ts->src/typings/database.ts - - + + - + src/core/trpc/procedures/api-config.procedure.ts->src/core/trpc/trpc.ts - - + + - + src/core/utils/package-json.ts - - -package-json.ts + + +package-json.ts - + src/core/trpc/procedures/api-config.procedure.ts->src/core/utils/package-json.ts - - + + - + src/core/utils/package-json.ts->package.json - - + + - + src/core/trpc/procedures/docker-manager.procedure.ts->src/core/utils/logger.ts - - + + - + src/core/trpc/procedures/docker-manager.procedure.ts->src/core/database/repository.ts - - + + - + src/core/trpc/procedures/docker-manager.procedure.ts->src/core/trpc/trpc.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/utils/logger.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/database/repository.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/docker.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/docker/client.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/utils/calculations.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/dockerode.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/trpc/trpc.ts - - + + - + src/core/trpc/procedures/logs.procedure.ts->src/core/utils/logger.ts - - + + - + src/core/trpc/procedures/logs.procedure.ts->src/core/database/repository.ts - - + + - + src/core/trpc/procedures/logs.procedure.ts->src/core/trpc/trpc.ts - - + + - + src/core/trpc/procedures/stacks.procedure.ts->src/core/utils/logger.ts - - + + - + src/core/trpc/procedures/stacks.procedure.ts->src/core/database/repository.ts - - + + - + src/core/trpc/procedures/stacks.procedure.ts->src/core/stacks/controller.ts - - + + - + src/core/trpc/procedures/stacks.procedure.ts->src/core/trpc/trpc.ts - - + + - + src/core/utils/respone-handler.ts - - -respone-handler.ts + + +respone-handler.ts - + src/core/utils/respone-handler.ts->src/core/utils/logger.ts - - + + - + src/typings/elysiajs.ts - - -elysiajs.ts + + +elysiajs.ts - + src/core/utils/respone-handler.ts->src/typings/elysiajs.ts - - + + - + src/index.ts - - -index.ts + + +index.ts - + src/index.ts->src/core/utils/logger.ts - - + + - + src/index.ts->src/core/database/repository.ts - - + + - + src/index.ts->src/typings/database.ts - - + + + + + +src/index.ts->src/core/docker/monitor.ts + + - + src/index.ts->src/core/docker/scheduler.ts - - + + - + src/index.ts->src/core/plugins/loader.ts - - + + - + src/index.ts->src/core/trpc/index.ts - - + + - + src/middleware/auth.ts - - -auth.ts + + +auth.ts - + src/index.ts->src/middleware/auth.ts - - + + - + src/routes/stacks.ts - - -stacks.ts + + +stacks.ts - + src/index.ts->src/routes/stacks.ts - - + + - + src/routes/api-config.ts - - -api-config.ts + + +api-config.ts - + src/index.ts->src/routes/api-config.ts - - + + - + src/routes/docker-manager.ts - - -docker-manager.ts + + +docker-manager.ts - + src/index.ts->src/routes/docker-manager.ts - - + + - + src/routes/docker-stats.ts - - -docker-stats.ts + + +docker-stats.ts - + src/index.ts->src/routes/docker-stats.ts - - + + - + src/routes/docker-websocket.ts - - -docker-websocket.ts + + +docker-websocket.ts - + src/index.ts->src/routes/docker-websocket.ts - - + + - + src/routes/logs.ts - - -logs.ts + + +logs.ts - + src/index.ts->src/routes/logs.ts - - + + - + src/middleware/auth.ts->src/core/utils/logger.ts - - + + - + src/middleware/auth.ts->src/core/database/repository.ts - - + + - + src/middleware/auth.ts->src/typings/database.ts - - + + - + src/middleware/auth.ts->src/typings/elysiajs.ts - - + + - + src/routes/stacks.ts->src/core/utils/logger.ts - - + + - + src/routes/stacks.ts->src/core/database/repository.ts - - + + - + src/routes/stacks.ts->src/core/stacks/controller.ts - - + + - + src/routes/stacks.ts->src/core/utils/respone-handler.ts - - + + - + src/routes/api-config.ts->src/core/utils/logger.ts - - + + - + src/routes/api-config.ts->src/core/database/repository.ts - - + + - + src/routes/api-config.ts->src/typings/database.ts - - + + - + src/routes/api-config.ts->src/core/utils/package-json.ts - - + + - + src/routes/api-config.ts->src/core/utils/respone-handler.ts - - + + - + src/routes/api-config.ts->src/middleware/auth.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-manager.ts->src/core/database/repository.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/respone-handler.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-stats.ts->src/core/database/repository.ts - - + + - + src/routes/docker-stats.ts->src/typings/docker.ts - - + + - + src/routes/docker-stats.ts->src/core/docker/client.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-stats.ts->src/typings/dockerode.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/respone-handler.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-websocket.ts->src/core/database/repository.ts - - + + - + src/routes/docker-websocket.ts->src/typings/docker.ts - - + + - + src/routes/docker-websocket.ts->src/core/docker/client.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/respone-handler.ts - - + + - + src/typings/websocket.ts - - -websocket.ts + + +websocket.ts - + src/routes/docker-websocket.ts->src/typings/websocket.ts - - + + - + stream - - -stream + + +stream - + src/routes/docker-websocket.ts->stream - - + + - + src/routes/logs.ts->src/core/utils/logger.ts - - + + - + src/routes/logs.ts->src/core/database/repository.ts - - + + - + src/typings/websocket.ts->stream - - + + From 16c94da6e4ba81d793b1417355ca752cd6161694 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 18 Mar 2025 22:55:11 +0100 Subject: [PATCH 193/324] Fix: Add routes to get all correctly imported plugins --- src/core/plugins/plugin-manager.ts | 9 ++++++--- src/routes/api-config.ts | 18 +++++++++++++++++- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/core/plugins/plugin-manager.ts b/src/core/plugins/plugin-manager.ts index c030b09..f7b8f95 100644 --- a/src/core/plugins/plugin-manager.ts +++ b/src/core/plugins/plugin-manager.ts @@ -1,8 +1,7 @@ import { EventEmitter } from "events"; import { logger } from "../utils/logger"; import type { Plugin } from "~/typings/plugin"; -import type { ContainerInfo, HostStats } from "~/typings/docker"; -import { plugin } from "bun"; +import type { ContainerInfo } from "~/typings/docker"; export class PluginManager extends EventEmitter { private plugins: Map = new Map(); @@ -22,6 +21,10 @@ export class PluginManager extends EventEmitter { this.plugins.delete(name); } + getLoadedPlugins(): string[] { + return Array.from(this.plugins.keys()); + } + // Trigger plugin flows: handleContainerStop(containerInfo: ContainerInfo) { this.plugins.forEach((plugin) => { @@ -106,7 +109,7 @@ export class PluginManager extends EventEmitter { plugin.onContainerKill?.(containerInfo); }); } - + handleContainerDie(containerInfo: ContainerInfo) { this.plugins.forEach((plugin) => { plugin.handleContainerDie?.(containerInfo); diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index 1c3b13b..093e390 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -15,6 +15,7 @@ import { license, } from "~/core/utils/package-json"; import { hashApiKey } from "~/middleware/auth"; +import { pluginManager } from "~/core/plugins/plugin-manager"; export const apiConfigRoutes = new Elysia({ prefix: "/config" }) .get( @@ -30,8 +31,8 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) } catch (error) { return responseHandler.error( set, - "Error getting the DockStatAPI config", error as string, + "Error getting the DockStatAPI config", ); } }, @@ -39,6 +40,21 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) tags: ["Management"], }, ) + .get( + "/plugins", + ({ set }) => { + try { + return pluginManager.getLoadedPlugins(); + } catch (error) { + return responseHandler.error( + set, + error as string, + "Error getting all registered plugins", + ); + } + }, + { tags: ["Management"] }, + ) .post( "/update", async ({ set, body }) => { From 06efcc22b726ce3d52b2756559bceefb3236ae05 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Tue, 18 Mar 2025 21:56:45 +0000 Subject: [PATCH 194/324] Update dependency graphs --- dependency-graph.dot | 2 +- dependency-graph.mmd | 2 +- dependency-graph.svg | 892 +++++++++++++++++++++---------------------- 3 files changed, 448 insertions(+), 448 deletions(-) diff --git a/dependency-graph.dot b/dependency-graph.dot index 06b5870..8759b82 100644 --- a/dependency-graph.dot +++ b/dependency-graph.dot @@ -55,7 +55,6 @@ strict digraph "dependency-cruiser output"{ "src/core/plugins/plugin-manager.ts" -> "src/core/utils/logger.ts" "src/core/plugins/plugin-manager.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] "src/core/plugins/plugin-manager.ts" -> "src/typings/plugin.ts" [arrowhead="onormal" penwidth="1.0"] - "src/core/plugins/plugin-manager.ts" -> "bun" "src/core/plugins/plugin-manager.ts" -> "events" [style="dashed" penwidth="1.0"] subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/stacks" {label="stacks" "src/core/stacks/controller.ts" [label= tooltip="controller.ts" URL="src/core/stacks/controller.ts" fillcolor="#ddfeff"] } } } "src/core/stacks/controller.ts" -> "src/core/database/repository.ts" @@ -133,6 +132,7 @@ strict digraph "dependency-cruiser output"{ "src/middleware/auth.ts" -> "src/typings/elysiajs.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/api-config.ts" [label= tooltip="api-config.ts" URL="src/routes/api-config.ts" fillcolor="#ddfeff"] } } "src/routes/api-config.ts" -> "src/core/database/repository.ts" + "src/routes/api-config.ts" -> "src/core/plugins/plugin-manager.ts" "src/routes/api-config.ts" -> "src/core/utils/logger.ts" "src/routes/api-config.ts" -> "src/core/utils/package-json.ts" "src/routes/api-config.ts" -> "src/core/utils/respone-handler.ts" diff --git a/dependency-graph.mmd b/dependency-graph.mmd index 36b5db9..0e9f516 100644 --- a/dependency-graph.mmd +++ b/dependency-graph.mmd @@ -101,7 +101,6 @@ end 7-->A 7-->I 7-->J -7-->5 7-->8 A-->D A-->B @@ -179,6 +178,7 @@ Z-->B 1E-->R 1E-->A 1F-->D +1F-->7 1F-->A 1F-->19 1F-->T diff --git a/dependency-graph.svg b/dependency-graph.svg index 1537421..7ea0c59 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -4,82 +4,82 @@ - - + + dependency-cruiser output - + cluster_fs - -fs + +fs cluster_src - -src + +src cluster_src/core - -core + +core cluster_src/core/database - -database + +database cluster_src/core/docker - -docker + +docker cluster_src/core/plugins - -plugins + +plugins cluster_src/core/stacks - -stacks + +stacks cluster_src/core/trpc - -trpc + +trpc cluster_src/core/trpc/procedures - -procedures + +procedures cluster_src/core/utils - -utils + +utils cluster_src/middleware - -middleware + +middleware cluster_src/routes - -routes + +routes cluster_src/typings - -typings + +typings bun - -bun + +bun @@ -87,8 +87,8 @@ bun:sqlite - -bun:sqlite + +bun:sqlite @@ -96,8 +96,8 @@ events - -events + +events @@ -105,8 +105,8 @@ fs - -fs + +fs @@ -114,8 +114,8 @@ fs/promises - -promises + +promises @@ -123,8 +123,8 @@ package.json - -package.json + +package.json @@ -132,8 +132,8 @@ path - -path + +path @@ -141,8 +141,8 @@ src/core/database/helper.ts - -helper.ts + +helper.ts @@ -150,445 +150,439 @@ src/core/utils/logger.ts - -logger.ts + +logger.ts src/core/database/helper.ts->src/core/utils/logger.ts - - - - + + + + - + src/core/utils/logger.ts->path - - + + src/core/database/repository.ts - -repository.ts + +repository.ts - + src/core/utils/logger.ts->src/core/database/repository.ts - - - - + + + + src/core/database/repository.ts->bun:sqlite - - + + src/core/database/repository.ts->src/core/database/helper.ts - - - - + + + + src/core/database/repository.ts->src/core/utils/logger.ts - - - - + + + + src/typings/database.ts - -database.ts + +database.ts src/core/database/repository.ts->src/typings/database.ts - - + + src/typings/docker.ts - -docker.ts + +docker.ts src/core/database/repository.ts->src/typings/docker.ts - - + + src/core/docker/client.ts - -client.ts + +client.ts src/core/docker/client.ts->src/core/utils/logger.ts - - + + src/core/docker/client.ts->src/typings/docker.ts - - + + src/core/docker/monitor.ts - -monitor.ts + +monitor.ts src/core/docker/monitor.ts->bun - - + + src/core/docker/monitor.ts->src/core/utils/logger.ts - - + + src/core/docker/monitor.ts->src/core/database/repository.ts - - + + src/core/docker/monitor.ts->src/typings/docker.ts - - + + src/core/docker/monitor.ts->src/core/docker/client.ts - - + + src/core/plugins/plugin-manager.ts - -plugin-manager.ts + +plugin-manager.ts src/core/docker/monitor.ts->src/core/plugins/plugin-manager.ts - - - - - -src/core/plugins/plugin-manager.ts->bun - - + + - + src/core/plugins/plugin-manager.ts->events - - + + src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts - - + + src/core/plugins/plugin-manager.ts->src/typings/docker.ts - - + + src/typings/plugin.ts - -plugin.ts + +plugin.ts src/core/plugins/plugin-manager.ts->src/typings/plugin.ts - - + + src/core/docker/scheduler.ts - -scheduler.ts + +scheduler.ts src/core/docker/scheduler.ts->src/core/utils/logger.ts - - + + src/core/docker/scheduler.ts->src/core/database/repository.ts - - + + src/core/docker/scheduler.ts->src/typings/database.ts - - + + src/core/docker/store-host-stats.ts - -store-host-stats.ts + +store-host-stats.ts src/core/docker/scheduler.ts->src/core/docker/store-host-stats.ts - - + + src/core/docker/store-container-stats.ts - -store-container-stats.ts + +store-container-stats.ts src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts - - + + src/core/docker/store-host-stats.ts->src/core/utils/logger.ts - - + + src/core/docker/store-host-stats.ts->src/core/database/repository.ts - - + + src/core/docker/store-host-stats.ts->src/typings/docker.ts - - + + src/core/docker/store-host-stats.ts->src/core/docker/client.ts - - + + src/typings/dockerode.ts - -dockerode.ts + +dockerode.ts src/core/docker/store-host-stats.ts->src/typings/dockerode.ts - - + + src/core/docker/store-container-stats.ts->src/core/database/repository.ts - - + + src/core/docker/store-container-stats.ts->src/core/docker/client.ts - - + + src/core/utils/calculations.ts - -calculations.ts + +calculations.ts src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts - - + + src/core/plugins/loader.ts - -loader.ts + +loader.ts src/core/plugins/loader.ts->fs - - + + src/core/plugins/loader.ts->path - - + + src/core/plugins/loader.ts->src/core/utils/logger.ts - - + + src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts - - + + src/core/utils/change-me-checker.ts - -change-me-checker.ts + +change-me-checker.ts src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts - - + + - + src/core/utils/change-me-checker.ts->fs/promises - - + + - + src/core/utils/change-me-checker.ts->src/core/utils/logger.ts - - + + src/typings/plugin.ts->src/typings/docker.ts - - + + src/core/stacks/controller.ts - -controller.ts + +controller.ts - + src/core/stacks/controller.ts->src/core/utils/logger.ts - - + + - + src/core/stacks/controller.ts->src/core/database/repository.ts - - + + - + src/core/stacks/controller.ts->src/typings/database.ts - - + + src/typings/docker-compose.ts - -docker-compose.ts + +docker-compose.ts - + src/core/stacks/controller.ts->src/typings/docker-compose.ts - - + + src/core/trpc/index.ts - -index.ts + +index.ts @@ -596,634 +590,640 @@ src/core/trpc/router.ts - -router.ts + +router.ts - + src/core/trpc/index.ts->src/core/trpc/router.ts - - + + src/core/trpc/procedures/api-config.procedure.ts - -api-config.procedure.ts + +api-config.procedure.ts - + src/core/trpc/router.ts->src/core/trpc/procedures/api-config.procedure.ts - - + + src/core/trpc/trpc.ts - -trpc.ts + +trpc.ts - + src/core/trpc/router.ts->src/core/trpc/trpc.ts - - + + src/core/trpc/procedures/docker-manager.procedure.ts - -docker-manager.procedure.ts + +docker-manager.procedure.ts - + src/core/trpc/router.ts->src/core/trpc/procedures/docker-manager.procedure.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts - -docker-stats.procedure.ts + +docker-stats.procedure.ts - + src/core/trpc/router.ts->src/core/trpc/procedures/docker-stats.procedure.ts - - + + src/core/trpc/procedures/logs.procedure.ts - -logs.procedure.ts + +logs.procedure.ts - + src/core/trpc/router.ts->src/core/trpc/procedures/logs.procedure.ts - - + + src/core/trpc/procedures/stacks.procedure.ts - -stacks.procedure.ts + +stacks.procedure.ts - + src/core/trpc/router.ts->src/core/trpc/procedures/stacks.procedure.ts - - + + - + src/core/trpc/procedures/api-config.procedure.ts->src/core/utils/logger.ts - - + + - + src/core/trpc/procedures/api-config.procedure.ts->src/core/database/repository.ts - - + + - + src/core/trpc/procedures/api-config.procedure.ts->src/typings/database.ts - - + + - + src/core/trpc/procedures/api-config.procedure.ts->src/core/trpc/trpc.ts - - + + src/core/utils/package-json.ts - -package-json.ts + +package-json.ts - + src/core/trpc/procedures/api-config.procedure.ts->src/core/utils/package-json.ts - - + + - + src/core/utils/package-json.ts->package.json - - + + - + src/core/trpc/procedures/docker-manager.procedure.ts->src/core/utils/logger.ts - - + + - + src/core/trpc/procedures/docker-manager.procedure.ts->src/core/database/repository.ts - - + + - + src/core/trpc/procedures/docker-manager.procedure.ts->src/core/trpc/trpc.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/utils/logger.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/database/repository.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/docker.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/docker/client.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/utils/calculations.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/dockerode.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/trpc/trpc.ts - - + + - + src/core/trpc/procedures/logs.procedure.ts->src/core/utils/logger.ts - - + + - + src/core/trpc/procedures/logs.procedure.ts->src/core/database/repository.ts - - + + - + src/core/trpc/procedures/logs.procedure.ts->src/core/trpc/trpc.ts - - + + - + src/core/trpc/procedures/stacks.procedure.ts->src/core/utils/logger.ts - - + + - + src/core/trpc/procedures/stacks.procedure.ts->src/core/database/repository.ts - - + + - + src/core/trpc/procedures/stacks.procedure.ts->src/core/stacks/controller.ts - - + + - + src/core/trpc/procedures/stacks.procedure.ts->src/core/trpc/trpc.ts - - + + src/core/utils/respone-handler.ts - -respone-handler.ts + +respone-handler.ts - + src/core/utils/respone-handler.ts->src/core/utils/logger.ts - - + + src/typings/elysiajs.ts - -elysiajs.ts + +elysiajs.ts - + src/core/utils/respone-handler.ts->src/typings/elysiajs.ts - - + + src/index.ts - -index.ts + +index.ts - + src/index.ts->src/core/utils/logger.ts - - + + - + src/index.ts->src/core/database/repository.ts - - + + - + src/index.ts->src/typings/database.ts - - + + - + src/index.ts->src/core/docker/monitor.ts - - + + - + src/index.ts->src/core/docker/scheduler.ts - - + + - + src/index.ts->src/core/plugins/loader.ts - - + + - + src/index.ts->src/core/trpc/index.ts - - + + src/middleware/auth.ts - -auth.ts + +auth.ts - + src/index.ts->src/middleware/auth.ts - - + + src/routes/stacks.ts - -stacks.ts + +stacks.ts - + src/index.ts->src/routes/stacks.ts - - + + src/routes/api-config.ts - -api-config.ts + +api-config.ts - + src/index.ts->src/routes/api-config.ts - - + + src/routes/docker-manager.ts - -docker-manager.ts + +docker-manager.ts - + src/index.ts->src/routes/docker-manager.ts - - + + src/routes/docker-stats.ts - -docker-stats.ts + +docker-stats.ts - + src/index.ts->src/routes/docker-stats.ts - - + + src/routes/docker-websocket.ts - -docker-websocket.ts + +docker-websocket.ts - + src/index.ts->src/routes/docker-websocket.ts - - + + src/routes/logs.ts - -logs.ts + +logs.ts - + src/index.ts->src/routes/logs.ts - - + + - + src/middleware/auth.ts->src/core/utils/logger.ts - - + + - + src/middleware/auth.ts->src/core/database/repository.ts - - + + - + src/middleware/auth.ts->src/typings/database.ts - - + + - + src/middleware/auth.ts->src/typings/elysiajs.ts - - + + src/routes/stacks.ts->src/core/utils/logger.ts - - + + src/routes/stacks.ts->src/core/database/repository.ts - - + + src/routes/stacks.ts->src/core/stacks/controller.ts - - + + src/routes/stacks.ts->src/core/utils/respone-handler.ts - - + + src/routes/api-config.ts->src/core/utils/logger.ts - - + + - + src/routes/api-config.ts->src/core/database/repository.ts - - + + src/routes/api-config.ts->src/typings/database.ts - - + + + + + +src/routes/api-config.ts->src/core/plugins/plugin-manager.ts + + src/routes/api-config.ts->src/core/utils/package-json.ts - - + + src/routes/api-config.ts->src/core/utils/respone-handler.ts - - + + src/routes/api-config.ts->src/middleware/auth.ts - - + + src/routes/docker-manager.ts->src/core/utils/logger.ts - - + + src/routes/docker-manager.ts->src/core/database/repository.ts - - + + src/routes/docker-manager.ts->src/core/utils/respone-handler.ts - - + + src/routes/docker-stats.ts->src/core/utils/logger.ts - - + + src/routes/docker-stats.ts->src/core/database/repository.ts - - + + src/routes/docker-stats.ts->src/typings/docker.ts - - + + src/routes/docker-stats.ts->src/core/docker/client.ts - - + + src/routes/docker-stats.ts->src/core/utils/calculations.ts - - + + src/routes/docker-stats.ts->src/typings/dockerode.ts - - + + src/routes/docker-stats.ts->src/core/utils/respone-handler.ts - - + + src/routes/docker-websocket.ts->src/core/utils/logger.ts - - + + src/routes/docker-websocket.ts->src/core/database/repository.ts - - + + src/routes/docker-websocket.ts->src/typings/docker.ts - - + + src/routes/docker-websocket.ts->src/core/docker/client.ts - - + + src/routes/docker-websocket.ts->src/core/utils/calculations.ts - - + + src/routes/docker-websocket.ts->src/core/utils/respone-handler.ts - - + + src/typings/websocket.ts - -websocket.ts + +websocket.ts src/routes/docker-websocket.ts->src/typings/websocket.ts - - + + @@ -1237,26 +1237,26 @@ src/routes/docker-websocket.ts->stream - - + + src/routes/logs.ts->src/core/utils/logger.ts - - + + src/routes/logs.ts->src/core/database/repository.ts - - + + src/typings/websocket.ts->stream - - + + From a23282c73297b835d64f44623198f84cd56735d3 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 19 Mar 2025 09:49:43 +0100 Subject: [PATCH 195/324] Fix: Sourcery suggesions, knip adjustments, stack controller refactor, swagger descriptions --- .knip.json | 5 +- bun.lock | 161 ++--------- package.json | 7 +- src/core/database/repository.ts | 45 ++-- src/core/docker/monitor.ts | 4 +- src/core/plugins/plugin-manager.ts | 2 +- src/core/stacks/controller.ts | 267 ++++++++++++------- src/core/trpc/procedures/stacks.procedure.ts | 17 +- src/core/trpc/router.ts | 2 - src/core/trpc/trpc.ts | 2 +- src/core/utils/respone-handler.ts | 6 +- src/routes/api-config.ts | 17 +- src/routes/docker-manager.ts | 3 + src/routes/docker-stats.ts | 3 + src/routes/logs.ts | 5 + src/routes/stacks.ts | 91 ++++--- src/typings/database.ts | 10 +- 17 files changed, 316 insertions(+), 331 deletions(-) diff --git a/.knip.json b/.knip.json index 14f7f53..64b44a6 100644 --- a/.knip.json +++ b/.knip.json @@ -1,4 +1,5 @@ { "entry": ["src/index.ts"], - "project": ["src/**/*.ts"] -} \ No newline at end of file + "project": ["src/**/*.ts"], + "ignore": ["src/plugins/*.plugin.ts"] +} diff --git a/bun.lock b/bun.lock index c9f06ca..9a3a82f 100644 --- a/bun.lock +++ b/bun.lock @@ -8,12 +8,12 @@ "@elysiajs/static": "^1.2.0", "@elysiajs/swagger": "^1.2.2", "@elysiajs/trpc": "^1.1.0", - "@elysiajs/websocket": "^0.2.8", "@trpc/server": "^10.45.2", "chalk": "^5.4.1", "docker-compose": "^1.1.1", "dockerode": "^4.0.4", "elysia": "latest", + "knip": "latest", "split2": "^4.2.0", "winston": "^3.17.0", "yaml": "^2.7.0", @@ -24,10 +24,9 @@ "@types/split2": "^4.2.3", "bun-types": "latest", "cross-env": "^7.0.3", - "dependency-cruiser": "^16.10.0", - "knip": "^5.46.0", "typescript": "^5.8.2", "wrap-ansi": "^9.0.0", + "zod": "^3.24.2", }, }, }, @@ -49,9 +48,7 @@ "@elysiajs/trpc": ["@elysiajs/trpc@1.1.0", "", { "peerDependencies": { "elysia": ">= 1.1.0" } }, "sha512-M8QWC+Wa5Z5MWY/+uMQuwZ+JoQkp4jOc1ra4SncFy1zSjFGin59LO1AT0pE+DRJaFV17gha9y7cB6Q7GnaJEAw=="], - "@elysiajs/websocket": ["@elysiajs/websocket@0.2.8", "", { "dependencies": { "nanoid": "^4.0.0", "raikiri": "^0.0.0-beta.3" }, "peerDependencies": { "elysia": ">= 0.2.2" } }, "sha512-K9KLmYL1SYuAV353GvmK0V9DG5w7XTOGsa1H1dGB5BUTzvBaMvnwNeqnJQ3cjf9V1c0EjQds0Ty4LfUFvV45jw=="], - - "@grpc/grpc-js": ["@grpc/grpc-js@1.12.6", "", { "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-JXUj6PI0oqqzTGvKtzOkxtpsyPRNsrmhh41TtIz/zEB6J+AUiZZ0dxWzcMwO9Ns5rmSPuMdghlTbUuqIM48d3Q=="], + "@grpc/grpc-js": ["@grpc/grpc-js@1.13.0", "", { "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-pMuxInZjUnUkgMT2QLZclRqwk2ykJbIU05aZgPgJYXEpN9+2I7z7aNwcjWZSycRPl232FfhPszyBFJyOxTHNog=="], "@grpc/proto-loader": ["@grpc/proto-loader@0.7.13", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.2.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw=="], @@ -85,11 +82,11 @@ "@scalar/openapi-types": ["@scalar/openapi-types@0.1.1", "", {}, "sha512-NMy3QNk6ytcCoPUGJH0t4NNr36OWXgZhA3ormr3TvhX1NDgoF95wFyodGVH8xiHeUyn2/FxtETm8UBLbB5xEmg=="], - "@scalar/themes": ["@scalar/themes@0.9.68", "", { "dependencies": { "@scalar/types": "0.0.34" } }, "sha512-466ac2fdQJOBBSLkGUf88vuZVF+qNMeVpjb0aAHrKkxhpjucTPKdTYO8r2dsX1R5k9A13gWPnm594VW5G/bGHw=="], + "@scalar/themes": ["@scalar/themes@0.9.80", "", { "dependencies": { "@scalar/types": "0.1.2" } }, "sha512-UZM8pQLpGeBtOdUx6yOcj5SPiWo1LaylUVt8HjCRFQ90zZtwbcIWfUWwWOay5nh7cwSVqY2G9eAyGYcNJB12ew=="], "@scalar/types": ["@scalar/types@0.0.12", "", { "dependencies": { "@scalar/openapi-types": "0.1.1", "@unhead/schema": "^1.9.5" } }, "sha512-XYZ36lSEx87i4gDqopQlGCOkdIITHHEvgkuJFrXFATQs9zHARop0PN0g4RZYWj+ZpCUclOcaOjbCt8JGe22mnQ=="], - "@sinclair/typebox": ["@sinclair/typebox@0.34.27", "", {}, "sha512-C7mxE1VC3WC2McOufZXEU48IfRVI+BcKxk4NOyNn3+JMUNdJHEWGS5CqjuDX+ij2NCCz8/nse1mT7yn8Fv2GHg=="], + "@sinclair/typebox": ["@sinclair/typebox@0.34.30", "", {}, "sha512-gFB3BiqjDxEoadW0zn+xyMVb7cLxPCoblVn2C/BKpI41WPYi2d6fwHAlynPNZ5O/Q4WEiujdnJzVtvG/Jc2CBQ=="], "@snyk/github-codeowners": ["@snyk/github-codeowners@1.1.0", "", { "dependencies": { "commander": "^4.1.1", "ignore": "^5.1.8", "p-map": "^4.0.0" }, "bin": { "github-codeowners": "dist/cli.js" } }, "sha512-lGFf08pbkEac0NYgVf4hdANpAgApRjNByLXB+WBip3qj1iendOIyAwP2GKkKbQMNVy2r1xxDf0ssfWscoiC+Vw=="], @@ -97,7 +94,7 @@ "@types/docker-modem": ["@types/docker-modem@3.0.6", "", { "dependencies": { "@types/node": "*", "@types/ssh2": "*" } }, "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg=="], - "@types/dockerode": ["@types/dockerode@3.3.34", "", { "dependencies": { "@types/docker-modem": "*", "@types/node": "*", "@types/ssh2": "*" } }, "sha512-mH9SuIb8NuTDsMus5epcbTzSbEo52fKLBMo0zapzYIAIyfDqoIFn7L3trekHLKC8qmxGV++pPUP4YqQ9n5v2Zg=="], + "@types/dockerode": ["@types/dockerode@3.3.35", "", { "dependencies": { "@types/docker-modem": "*", "@types/node": "*", "@types/ssh2": "*" } }, "sha512-P+DCMASlsH+QaKkDpekKrP5pLls767PPs+/LrlVbKnEnY5tMpEUa2C6U4gRsdFZengOqxdCIqy16R22Q3pLB6Q=="], "@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="], @@ -109,23 +106,11 @@ "@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="], - "@unhead/schema": ["@unhead/schema@1.11.19", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-7VhYHWK7xHgljdv+C01MepCSYZO2v6OhgsfKWPxRQBDDGfUKCUaChox0XMq3tFvXP6u4zSp6yzcDw2yxCfVMwg=="], - - "acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="], - - "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], - - "acorn-jsx-walk": ["acorn-jsx-walk@2.0.0", "", {}, "sha512-uuo6iJj4D4ygkdzd6jPtcxs8vZgDX9YFIkqczGImoypX2fQ4dVImmu3UzA4ynixCIMTrEOWW+95M2HuBaCEOVA=="], - - "acorn-loose": ["acorn-loose@8.4.0", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-M0EUka6rb+QC4l9Z3T0nJEzNOO7JcoJlYMrBlyBCiFSXRyxjLKayd4TbQs2FDRWQU1h9FR7QVNHt+PEaoNL5rQ=="], - - "acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="], + "@unhead/schema": ["@unhead/schema@1.11.20", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA=="], "aggregate-error": ["aggregate-error@3.1.0", "", { "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" } }, "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA=="], - "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], - - "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], "ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], @@ -147,7 +132,7 @@ "buildcheck": ["buildcheck@0.0.6", "", {}, "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A=="], - "bun-types": ["bun-types@1.2.3", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-P7AeyTseLKAvgaZqQrvp3RqFM3yN9PlcLuSTe7SoJOfZkER73mLdT2vEQi8U64S1YvM/ldcNiQjn0Sn7H9lGgg=="], + "bun-types": ["bun-types@1.2.5", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-3oO6LVGGRRKI4kHINx5PIdIgnLRb7l/SprhzqXapmoYkFl5m4j6EvALvbDVuuBFaamB46Ap6HCUxIXNLCGy+tg=="], "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], @@ -161,15 +146,15 @@ "color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="], - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + "color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], - "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], "colorspace": ["colorspace@1.1.4", "", { "dependencies": { "color": "^3.1.3", "text-hex": "1.0.x" } }, "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w=="], - "commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="], + "commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], @@ -183,9 +168,7 @@ "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], - "dependency-cruiser": ["dependency-cruiser@16.10.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "acorn-jsx-walk": "^2.0.0", "acorn-loose": "^8.4.0", "acorn-walk": "^8.3.4", "ajv": "^8.17.1", "commander": "^13.1.0", "enhanced-resolve": "^5.18.1", "ignore": "^7.0.3", "interpret": "^3.1.1", "is-installed-globally": "^1.0.0", "json5": "^2.2.3", "memoize": "^10.0.0", "picocolors": "^1.1.1", "picomatch": "^4.0.2", "prompts": "^2.4.2", "rechoir": "^0.8.0", "safe-regex": "^2.1.1", "semver": "^7.7.1", "teamcity-service-messages": "^0.1.14", "tsconfig-paths-webpack-plugin": "^4.2.0", "watskeburt": "^4.2.3" }, "bin": { "dependency-cruiser": "bin/dependency-cruise.mjs", "dependency-cruise": "bin/dependency-cruise.mjs", "depcruise": "bin/dependency-cruise.mjs", "depcruise-baseline": "bin/depcruise-baseline.mjs", "depcruise-fmt": "bin/depcruise-fmt.mjs", "depcruise-wrap-stream-in-html": "bin/wrap-stream-in-html.mjs" } }, "sha512-o6pEB8X/XS0AjpQBhPJW3pSY7HIviRM7+G601T9ruV63NVJC4DxLMA+a1VzZlKOzO2fO6JKRHjRmGjzZZHEFYA=="], - - "docker-compose": ["docker-compose@1.1.1", "", { "dependencies": { "yaml": "^2.2.2" } }, "sha512-UkIUz0LtzuO17Ijm6SXMGtfZMs7IvbNwvuJBiBuN93PIhr/n9/sbJMqpvYFaCBGfwu1ZM4PPPDgQzeeke4lEoA=="], + "docker-compose": ["docker-compose@1.2.0", "", { "dependencies": { "yaml": "^2.2.2" } }, "sha512-wIU1eHk3Op7dFgELRdmOYlPYS4gP8HhH1ZmZa13QZF59y0fblzFDFmKPhyc05phCy2hze9OEvNZAsoljrs+72w=="], "docker-modem": ["docker-modem@5.0.6", "", { "dependencies": { "debug": "^4.1.1", "readable-stream": "^3.5.0", "split-ca": "^1.0.1", "ssh2": "^1.15.0" } }, "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ=="], @@ -193,7 +176,7 @@ "easy-table": ["easy-table@1.2.0", "", { "dependencies": { "ansi-regex": "^5.0.1" }, "optionalDependencies": { "wcwidth": "^1.0.1" } }, "sha512-OFzVOv03YpvtcWGe5AayU5G2hgybsg3iqA6drU8UaoZyB9jLGMTrz9+asnLp/E+6qPh88yEI1gvyZFZ41dmgww=="], - "elysia": ["elysia@1.2.21", "", { "dependencies": { "@sinclair/typebox": "^0.34.27", "cookie": "^1.0.2", "memoirist": "^0.3.0", "openapi-types": "^12.1.3" }, "peerDependencies": { "typescript": ">= 5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-E9b1JcB7fiQ2ptk24W8OnBrMYUoKzffIXob9uTVUKhqOKxaXAd9UyWBeyr7JCDa/VD/b/9S8aIey9/YJsK5sLg=="], + "elysia": ["elysia@1.2.25", "", { "dependencies": { "@sinclair/typebox": "^0.34.27", "cookie": "^1.0.2", "memoirist": "^0.3.0", "openapi-types": "^12.1.3" }, "peerDependencies": { "typescript": ">= 5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-WsdQpORJvb4uszzeqYT0lg97knw1iBW1NTzJ1Jm57tiHg+DfAotlWXYbjmvQ039ssV0fYELDHinLLoUazZkEHg=="], "emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], @@ -205,12 +188,8 @@ "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], - "fast-uri": ["fast-uri@3.0.6", "", {}, "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw=="], - "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], "fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="], @@ -221,52 +200,34 @@ "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], - "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], "get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="], "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - "global-directory": ["global-directory@4.0.1", "", { "dependencies": { "ini": "4.1.1" } }, "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q=="], - "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], - "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - "ignore": ["ignore@7.0.3", "", {}, "sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA=="], + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - "ini": ["ini@4.1.1", "", {}, "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g=="], - - "interpret": ["interpret@3.1.1", "", {}, "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ=="], - "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], - "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], - "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], - "is-installed-globally": ["is-installed-globally@1.0.0", "", { "dependencies": { "global-directory": "^4.0.1", "is-path-inside": "^4.0.0" } }, "sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ=="], - "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], - "is-path-inside": ["is-path-inside@4.0.0", "", {}, "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA=="], - "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], @@ -275,12 +236,6 @@ "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], - "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - - "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], - - "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], - "knip": ["knip@5.46.0", "", { "dependencies": { "@nodelib/fs.walk": "3.0.1", "@snyk/github-codeowners": "1.1.0", "easy-table": "1.2.0", "enhanced-resolve": "^5.18.0", "fast-glob": "^3.3.3", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "minimist": "^1.2.8", "picocolors": "^1.1.0", "picomatch": "^4.0.1", "pretty-ms": "^9.0.0", "smol-toml": "^1.3.1", "strip-json-comments": "5.0.1", "summary": "2.1.0", "zod": "^3.22.4", "zod-validation-error": "^3.0.3" }, "peerDependencies": { "@types/node": ">=18", "typescript": ">=5.0.4" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-WedHSK5xNBWYgm64Rt5B9b0CVXL2kRBcyCeet3NHgdv9en3QE4AWSDPEiX48NoPUBW3h//9S0VwLF5MG/MPi3g=="], "kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="], @@ -293,23 +248,17 @@ "memoirist": ["memoirist@0.3.0", "", {}, "sha512-wR+4chMgVPq+T6OOsk40u9Wlpw1Pjx66NMNiYxCQQ4EUJ7jDs3D9kTCeKdBOkvAiqXlHLVJlvYL01PvIJ1MPNg=="], - "memoize": ["memoize@10.1.0", "", { "dependencies": { "mimic-function": "^5.0.1" } }, "sha512-MMbFhJzh4Jlg/poq1si90XRlTZRDHVqdlz2mPyGJ6kqMpyHUyVpDd5gpFAvVehW64+RA1eKE9Yt8aSLY7w2Kgg=="], - "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], - "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], - "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "nan": ["nan@2.22.1", "", {}, "sha512-pfRR4ZcNTSm2ZFHaztuvbICf+hyiG6ecA06SfAxoPmuHjvMu0KUIae7Y8GyVkbBqeEIidsmXeYooWIX9+qjfRQ=="], - - "nanoid": ["nanoid@4.0.2", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw=="], + "nan": ["nan@2.22.2", "", {}, "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ=="], "node-cache": ["node-cache@5.1.2", "", { "dependencies": { "clone": "2.x" } }, "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg=="], @@ -325,8 +274,6 @@ "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], - "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], @@ -335,50 +282,32 @@ "pretty-ms": ["pretty-ms@9.2.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg=="], - "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], - "protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], "pump": ["pump@3.0.2", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw=="], "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], - "raikiri": ["raikiri@0.0.0-beta.8", "", {}, "sha512-cH/yfvkiGkN8IBB2MkRHikpPurTnd2sMkQ/xtGpXrp3O76P4ppcWPb+86mJaBDzKaclLnSX+9NnT79D7ifH4/w=="], - "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - "rechoir": ["rechoir@0.8.0", "", { "dependencies": { "resolve": "^1.20.0" } }, "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ=="], - - "regexp-tree": ["regexp-tree@0.1.27", "", { "bin": { "regexp-tree": "bin/regexp-tree" } }, "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA=="], - "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], - "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], - - "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], - "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - "safe-regex": ["safe-regex@2.1.1", "", { "dependencies": { "regexp-tree": "~0.1.1" } }, "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A=="], - "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - "semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], - "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], - "smol-toml": ["smol-toml@1.3.1", "", {}, "sha512-tEYNll18pPKHroYSmLLrksq233j021G0giwW7P3D24jC54pQ5W5BXMsQ/Mvw1OJCmEYDgY+lrzT+3nNUtoNfXQ=="], "split-ca": ["split-ca@1.0.1", "", {}, "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ=="], @@ -395,34 +324,22 @@ "strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], - "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], - "strip-json-comments": ["strip-json-comments@5.0.1", "", {}, "sha512-0fk9zBqO67Nq5M/m45qHCJxylV/DhBlIOVExqgOMiCCrzrhU6tCibRXNqE3jwJLftzE9SNuZtYbpzcO+i9FiKw=="], "summary": ["summary@2.1.0", "", {}, "sha512-nMIjMrd5Z2nuB2RZCKJfFMjgS3fygbeyGk9PxPPaJR1RIcyN9yn4A63Isovzm3ZtQuEkLBVgMdPup8UeLH7aQw=="], - "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - "tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="], "tar-fs": ["tar-fs@2.0.1", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.0.0" } }, "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA=="], "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], - "teamcity-service-messages": ["teamcity-service-messages@0.1.14", "", {}, "sha512-29aQwaHqm8RMX74u2o/h1KbMLP89FjNiMxD9wbF2BbWOnbM+q+d1sCEC+MqCc4QW3NJykn77OMpTFw/xTHIc0w=="], - "text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], "triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="], - "tsconfig-paths": ["tsconfig-paths@4.2.0", "", { "dependencies": { "json5": "^2.2.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg=="], - - "tsconfig-paths-webpack-plugin": ["tsconfig-paths-webpack-plugin@4.2.0", "", { "dependencies": { "chalk": "^4.1.0", "enhanced-resolve": "^5.7.0", "tapable": "^2.2.1", "tsconfig-paths": "^4.1.2" } }, "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA=="], - "tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="], "typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], @@ -433,8 +350,6 @@ "uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], - "watskeburt": ["watskeburt@4.2.3", "", { "bin": { "watskeburt": "dist/run-cli.js" } }, "sha512-uG9qtQYoHqAsnT711nG5iZc/8M5inSmkGCOp7pFaytKG2aTfIca7p//CjiVzAE4P7hzaYuCozMjNNaLgmhbK5g=="], - "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], @@ -463,23 +378,9 @@ "@nodelib/fs.scandir/@nodelib/fs.stat": ["@nodelib/fs.stat@4.0.0", "", {}, "sha512-ctr6bByzksKRCV0bavi8WoQevU6plSp2IkllIsEqaiKe2mwNNnaluhnRhcsgGZHrrHk57B3lf95MkLMO3STYcg=="], - "@scalar/themes/@scalar/types": ["@scalar/types@0.0.34", "", { "dependencies": { "@scalar/openapi-types": "0.1.8", "@unhead/schema": "^1.11.11" } }, "sha512-q01ctijmHArM5KOny2zU+sHfhpsgOAENrDENecK2TsQNn5FYLmFZouMKeW2M6F7KFLPZnFxUiL/rT88b6Rp/Kg=="], - - "@snyk/github-codeowners/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], - - "@snyk/github-codeowners/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], - - "@types/docker-modem/@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], - - "@types/dockerode/@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], + "@scalar/themes/@scalar/types": ["@scalar/types@0.1.2", "", { "dependencies": { "@scalar/openapi-types": "0.1.9", "@unhead/schema": "^1.11.11", "zod": "^3.23.8" } }, "sha512-5kCLQRwAYWt1ds110EaUb9yonc3KoQYNyo4YUCigJLOnoNugbqkEX0zRudGevItiuk+xg4uOYd30r3C+6xAasA=="], - "@types/split2/@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], - - "@types/ssh2/@types/node": ["@types/node@18.19.76", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-yvR7Q9LdPz2vGpmpJX5LolrgRdWvB67MJKDPSgIIzpFbaf9a1j/f5DnLp5VDyHGMR0QZHlTr1afsD87QCXFHKw=="], - - "@types/ws/@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], - - "bun-types/@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], + "@types/ssh2/@types/node": ["@types/node@18.19.80", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-kEWeMwMeIvxYkeg1gTc01awpwLbfMRZXdIhwRcakd/KlK53jmRC26LqcbIt7fnAQTu5GzlnWmzA3H6+l1u6xxQ=="], "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -487,40 +388,36 @@ "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - "color/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], - - "color-string/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], - "defaults/clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], + "easy-table/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "fast-glob/@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "protobufjs/@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], - - "strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], - - "tsconfig-paths-webpack-plugin/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.1.8", "", {}, "sha512-iufA5/6hPCmRIVD2eh7qGpoKvoA08Gw/qUb2JECifBtAwA93fo7+1k9uHK440f2LMJsbxIzA+nv7RS0BmfiO/g=="], + "@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.1.9", "", {}, "sha512-HQQudOSQBU7ewzfnBW9LhDmBE2XOJgSfwrh5PlUB7zJup/kaRkBGNgV2wMjNz9Af/uztiU/xNrO179FysmUT+g=="], "@types/ssh2/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "color/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "fast-glob/@nodelib/fs.walk/@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], - "tsconfig-paths-webpack-plugin/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "cliui/wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "cliui/wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], } } diff --git a/package.json b/package.json index da50177..3426d54 100644 --- a/package.json +++ b/package.json @@ -25,12 +25,12 @@ "@elysiajs/static": "^1.2.0", "@elysiajs/swagger": "^1.2.2", "@elysiajs/trpc": "^1.1.0", - "@elysiajs/websocket": "^0.2.8", "@trpc/server": "^10.45.2", "chalk": "^5.4.1", "docker-compose": "^1.1.1", "dockerode": "^4.0.4", "elysia": "latest", + "knip": "latest", "split2": "^4.2.0", "winston": "^3.17.0", "yaml": "^2.7.0" @@ -41,10 +41,9 @@ "@types/split2": "^4.2.3", "bun-types": "latest", "cross-env": "^7.0.3", - "dependency-cruiser": "^16.10.0", - "knip": "^5.46.0", "typescript": "^5.8.2", - "wrap-ansi": "^9.0.0" + "wrap-ansi": "^9.0.0", + "zod": "^3.24.2" }, "module": "src/index.js", "trustedDependencies": [ diff --git a/src/core/database/repository.ts b/src/core/database/repository.ts index d5f11d6..552b6a2 100644 --- a/src/core/database/repository.ts +++ b/src/core/database/repository.ts @@ -155,8 +155,7 @@ export const dbFunctions = { FROM docker_hosts ORDER BY name DESC `); - const data = stmt.all(); - return data as DockerHost[]; + return stmt.all() as DockerHost[]; }, () => {}, ); @@ -194,8 +193,7 @@ export const dbFunctions = { FROM backend_log_entries ORDER BY timestamp DESC `); - const data = stmt.all(); - return data; + return stmt.all(); }, () => {}, ); @@ -211,8 +209,7 @@ export const dbFunctions = { WHERE level = ? ORDER BY timestamp DESC `); - const data = stmt.all(level); - return data; + return stmt.all(level); }, () => { if (typeof level !== "string") { @@ -232,8 +229,7 @@ export const dbFunctions = { SET url = ?, secure = ? WHERE name = ? `); - const data = stmt.run(url, secure, name); - return data; + return stmt.run(url, secure, name); }, () => { if ( @@ -256,8 +252,7 @@ export const dbFunctions = { DELETE FROM docker_hosts WHERE name = ? `); - const data = stmt.run(name); - return data; + return stmt.run(name); }, () => { if (typeof name !== "string") { @@ -275,8 +270,7 @@ export const dbFunctions = { const stmt = db.prepare(` DELETE FROM backend_log_entries `); - const data = stmt.run(); - return data; + return stmt.run(); }, () => {}, ); @@ -290,8 +284,7 @@ export const dbFunctions = { DELETE FROM backend_log_entries WHERE level = ? `); - const data = stmt.run(level); - return data; + return stmt.run(level); }, () => { if (typeof level !== "string") { @@ -316,8 +309,7 @@ export const dbFunctions = { keep_data_for = ?, api_key = ? `); - const data = stmt.run(fetching_interval, keep_data_for, api_key); - return data; + return stmt.run(fetching_interval, keep_data_for, api_key); }, () => { if ( @@ -339,8 +331,7 @@ export const dbFunctions = { SELECT keep_data_for, fetching_interval, api_key FROM config `); - const data = stmt.all(); - return data; + return stmt.all(); }, () => {}, ); @@ -388,7 +379,7 @@ export const dbFunctions = { INSERT INTO container_stats (id, hostId, name, image, status, state, cpu_usage, memory_usage) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `); - const data = stmt.run( + return stmt.run( id, hostId, name, @@ -398,7 +389,6 @@ export const dbFunctions = { cpu_usage, memory_usage, ); - return data; }, () => { if ( @@ -454,7 +444,7 @@ export const dbFunctions = { containersPaused = excluded.containersPaused, images = excluded.images; `); - const data = stmt.run( + return stmt.run( stats.hostId, stats.dockerVersion, stats.apiVersion, @@ -469,7 +459,6 @@ export const dbFunctions = { stats.containersPaused, stats.images, ); - return data; }, () => {}, ); @@ -492,7 +481,7 @@ export const dbFunctions = { ) VALUES(?, ?, ?, ?, ?, ?, ?, ?) `); - const data = stmt.run( + return stmt.run( stack_config.name, stack_config.version, stack_config.custom, @@ -502,7 +491,6 @@ export const dbFunctions = { stack_config.automatic_reboot_on_error, stack_config.image_updates, ); - return data; }, () => {}, ); @@ -517,8 +505,7 @@ export const dbFunctions = { FROM stacks_config ORDER BY name DESC `); - const data = stmt.all(); - return data; + return stmt.all(); }, () => {}, ); @@ -532,8 +519,7 @@ export const dbFunctions = { DELETE FROM stacks_config WHERE name = ?; `); - const data = stmt.run(name); - return data; + return stmt.run(name); }, () => {}, ); @@ -555,7 +541,7 @@ export const dbFunctions = { image_updates = ? WHERE name = ?; `); - const data = stmt.run( + return stmt.run( stack_config.version, stack_config.custom, stack_config.source, @@ -565,7 +551,6 @@ export const dbFunctions = { stack_config.image_updates, stack_config.name, ); - return data; }, () => {}, ); diff --git a/src/core/docker/monitor.ts b/src/core/docker/monitor.ts index fe8df25..4f56b2b 100644 --- a/src/core/docker/monitor.ts +++ b/src/core/docker/monitor.ts @@ -49,7 +49,9 @@ async function startFor(host: DockerHost) { buffer = lines.pop() || ""; for (const line of lines) { - if (line.trim() === "") continue; + if (line.trim() === "") { + continue; + } let event: any; try { diff --git a/src/core/plugins/plugin-manager.ts b/src/core/plugins/plugin-manager.ts index f7b8f95..55453d0 100644 --- a/src/core/plugins/plugin-manager.ts +++ b/src/core/plugins/plugin-manager.ts @@ -3,7 +3,7 @@ import { logger } from "../utils/logger"; import type { Plugin } from "~/typings/plugin"; import type { ContainerInfo } from "~/typings/docker"; -export class PluginManager extends EventEmitter { +class PluginManager extends EventEmitter { private plugins: Map = new Map(); register(plugin: Plugin) { diff --git a/src/core/stacks/controller.ts b/src/core/stacks/controller.ts index 2ff5edb..f4af21a 100644 --- a/src/core/stacks/controller.ts +++ b/src/core/stacks/controller.ts @@ -5,130 +5,195 @@ import DockerCompose from "docker-compose"; import type { Stack, ComposeSpec } from "~/typings/docker-compose"; import type { stacks_config } from "~/typings/database"; +async function runStackCommand( + stack_name: string, + command: (cwd: string) => Promise, + action: string, +): Promise { + try { + const stack = { name: stack_name }; + const stackPath = await getStackPath(stack as Stack); + return await command(stackPath); + } catch (error: any) { + throw new Error( + `Error while ${action} stack "${stack_name}": ${error.message || error}`, + ); + } +} + async function getStackPath(stack: Stack): Promise { - const stackName = stack.name.trim().replace(/\s+/g, "_"); - return `stacks/${stackName}`; + const stackName = stack.name.trim().replace(/\s+/g, "_"); + return `stacks/${stackName}`; } async function createStackYAML(compose_spec: Stack): Promise { - const yaml = YAML.stringify(compose_spec.compose_spec); - const stackPath = await getStackPath(compose_spec); - await Bun.write(`${stackPath}/docker-compose.yaml`, yaml, { createPath: true }); + const yaml = YAML.stringify(compose_spec.compose_spec); + const stackPath = await getStackPath(compose_spec); + await Bun.write(`${stackPath}/docker-compose.yaml`, yaml, { + createPath: true, + }); } export async function deployStack( - stack: ComposeSpec, - name: string, - version: number, - source: string, - automatic_reboot_on_error: boolean, - isCustom: boolean, - image_updates: boolean, - stack_prefix?: string + stack: ComposeSpec, + name: string, + version: number, + source: string, + automatic_reboot_on_error: boolean, + isCustom: boolean, + image_updates: boolean, + stack_prefix?: string, ): Promise { - try { - logger.debug(`Deploying Stack: ${JSON.stringify(stack)}`) - - const serviceCount = stack.services - ? Object.keys(stack.services).length - : 0; - - const resolvedPrefix = stack_prefix ?? ""; - - const stack_config: stacks_config = { - name: name, - version: version, - source, - stack_prefix: resolvedPrefix, - automatic_reboot_on_error, - container_count: serviceCount, - custom: isCustom, - image_updates, - }; - - if (!stack.name) { - logger.debug(`${JSON.stringify(stack)}`) - throw new Error("Stack name needed") - } - - dbFunctions.addStack(stack_config); - - const stackYaml: Stack = { - name: name, - source: source, - version: version, - compose_spec: stack, - } - await createStackYAML(stackYaml); - const stackPath = await getStackPath(stackYaml); - await DockerCompose.upAll({ cwd: stackPath }); - } catch (error: any) { - throw new Error(`Error while deploying Stack: ${error.message || error}`); + try { + logger.debug(`Deploying Stack: ${JSON.stringify(stack)}`); + + const serviceCount = stack.services + ? Object.keys(stack.services).length + : 0; + + const resolvedPrefix = stack_prefix ?? ""; + + const stack_config: stacks_config = { + name: name, + version: version, + source, + stack_prefix: resolvedPrefix, + automatic_reboot_on_error, + container_count: serviceCount, + custom: isCustom, + image_updates, + }; + + if (!stack.name) { + logger.debug(`${JSON.stringify(stack)}`); + throw new Error("Stack name needed"); } + + dbFunctions.addStack(stack_config); + + const stackYaml: Stack = { + name: name, + source: source, + version: version, + compose_spec: stack, + }; + await createStackYAML(stackYaml); + const stackPath = await getStackPath(stackYaml); + await DockerCompose.upAll({ cwd: stackPath }); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + throw new Error(errorMsg); + } } export async function stopStack(stack_name: string): Promise { - try { - const stack = { - name: stack_name - } - const stackPath = await getStackPath(stack as Stack); - await DockerCompose.downAll({ cwd: stackPath }); - } catch (error: any) { - throw new Error(`Error while stopping stack "${stack_name}": ${error.message || error}`); - } + try { + await runStackCommand( + stack_name, + (cwd) => DockerCompose.downAll({ cwd }), + "stopping", + ); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + throw new Error(errorMsg); + } } export async function startStack(stack_name: string): Promise { - try { - const stack = { - name: stack_name - } - const stackPath = await getStackPath(stack as Stack); - await DockerCompose.upAll({ cwd: stackPath }); - } catch (error: any) { - throw new Error(`Error while starting stack "${stack_name}": ${error.message || error}`); - } + try { + await runStackCommand( + stack_name, + (cwd) => DockerCompose.upAll({ cwd }), + "starting", + ); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + throw new Error(errorMsg); + } } export async function pullStackImages(stack_name: string): Promise { - try { - const stack = { - name: stack_name - } - const stackPath = await getStackPath(stack as Stack); - await DockerCompose.pullAll({ cwd: stackPath }); - } catch (error: any) { - throw new Error(`Error while pulling images for stack "${stack_name}": ${error.message || error}`); - } + try { + await runStackCommand( + stack_name, + (cwd) => DockerCompose.pullAll({ cwd }), + "pulling images for", + ); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + throw new Error(errorMsg); + } } export async function restartStack(stack_name: string): Promise { - try { - const stack = { - name: stack_name - } - const stackPath = await getStackPath(stack as Stack); - await DockerCompose.restartAll({ cwd: stackPath }); - } catch (error: any) { - throw new Error(`Error while restarting stack "${stack_name}": ${error.message || error}`); - } + try { + await runStackCommand( + stack_name, + (cwd) => DockerCompose.restartAll({ cwd }), + "restarting", + ); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + throw new Error(errorMsg); + } } -export async function getStackStatus(stack_name: string): Promise { - try { - logger.debug("Retrieving status for Stack:", stack_name); - const stackYaml = { name: stack_name }; - const stackPath = await getStackPath(stackYaml as Stack); - const rawStatus = await DockerCompose.ps({ cwd: stackPath }); - +export async function getStackStatus(stack_name: string): Promise { + try { + return await runStackCommand( + stack_name, + async (cwd) => { + const rawStatus = await DockerCompose.ps({ cwd }); return rawStatus.data.services.reduce((acc: any, service: any) => { - acc[(service.name)] = service.state; - return acc; - }, {}); - - } catch (error: any) { - throw new Error(`Error while retrieving status for stack "${stack_name}": ${error.message || error}`); - } + acc[service.name] = service.state; + return acc; + }, {}); + }, + "retrieving status for", + ); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + throw new Error(errorMsg); + } } +export async function getAllStacksStatus(): Promise> { + try { + const stacks = dbFunctions.getStacks() as stacks_config[]; + + const statusResults = await Promise.all( + stacks.map(async (stack) => { + const status = await runStackCommand( + stack.name, + async (cwd) => { + const rawStatus = await DockerCompose.ps({ cwd }); + return rawStatus.data.services.reduce((acc: any, service: any) => { + acc[service.name] = service.state; + return acc; + }, {}); + }, + "retrieving status for", + ); + return { stackName: stack.name, status }; + }), + ); + + return statusResults.reduce( + (acc, { stackName, status }) => { + acc[stackName] = status; + return acc; + }, + {} as Record, + ); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + throw new Error(errorMsg); + } +} diff --git a/src/core/trpc/procedures/stacks.procedure.ts b/src/core/trpc/procedures/stacks.procedure.ts index 6aad4e3..db7a85c 100644 --- a/src/core/trpc/procedures/stacks.procedure.ts +++ b/src/core/trpc/procedures/stacks.procedure.ts @@ -37,11 +37,18 @@ export const stacksProcedure = router({ .mutation(async ({ input }) => { try { const missingParams = []; - if (!input.compose_spec) missingParams.push("compose_spec"); - if (!input.automatic_reboot_on_error) + if (!input.compose_spec) { + missingParams.push("compose_spec"); + } + if (!input.automatic_reboot_on_error) { missingParams.push("automatic_reboot_on_error"); - if (!input.source) missingParams.push("source"); - if (!input.name) missingParams.push("name"); + } + if (!input.source) { + missingParams.push("source"); + } + if (!input.name) { + missingParams.push("name"); + } if (missingParams.length > 0) { throw new TRPCError({ @@ -58,7 +65,7 @@ export const stacksProcedure = router({ input.automatic_reboot_on_error, input.isCustom || false, input.image_updates || false, - input.stack_prefix + input.stack_prefix, ); logger.info(`Deployed Stack (${input.name}) via tRPC`); diff --git a/src/core/trpc/router.ts b/src/core/trpc/router.ts index acdd78d..9e0bddf 100644 --- a/src/core/trpc/router.ts +++ b/src/core/trpc/router.ts @@ -17,5 +17,3 @@ export const appRouter = router({ check: t.procedure.query(() => ({ status: "healthy" })), }), }); - -export type AppRouter = typeof appRouter; diff --git a/src/core/trpc/trpc.ts b/src/core/trpc/trpc.ts index 554f58d..c7813f9 100644 --- a/src/core/trpc/trpc.ts +++ b/src/core/trpc/trpc.ts @@ -1,5 +1,5 @@ import { initTRPC } from "@trpc/server"; export const t = initTRPC.create(); -export const router = t.router; +export const { router } = t; export const publicProcedure = t.procedure; diff --git a/src/core/utils/respone-handler.ts b/src/core/utils/respone-handler.ts index 65b7c09..369e917 100644 --- a/src/core/utils/respone-handler.ts +++ b/src/core/utils/respone-handler.ts @@ -19,10 +19,10 @@ export const responseHandler = { return { success: true }; }, - simple_error(set: set, response_massage: string, status_code?: number) { + simple_error(set: set, response_message: string, status_code?: number) { set.status = status_code || 502; - logger.warn(response_massage); - return { error: response_massage }; + logger.warn(response_message); + return { error: response_message }; }, reject(set: set, reject: any, response_message: string, error?: string) { diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index 093e390..a74f34b 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -37,7 +37,10 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) } }, { - tags: ["Management"], + detail: { + tags: ["Management"], + description: "Returns DockStatAPI's config", + }, }, ) .get( @@ -53,7 +56,7 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) ); } }, - { tags: ["Management"] }, + { detail: { tags: ["Management"], description: "List all Plugin Names" } }, ) .post( "/update", @@ -81,7 +84,10 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) keep_data_for: t.Number(), api_key: t.String(), }), - tags: ["Management"], + detail: { + tags: ["Management"], + description: "Update the current DockStatAPI config", + }, }, ) .get( @@ -109,6 +115,9 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) } }, { - tags: ["Management"], + detail: { + tags: ["Management"], + description: "Returns relevant information about the package.json", + }, }, ); diff --git a/src/routes/docker-manager.ts b/src/routes/docker-manager.ts index eb53fdb..1d35101 100644 --- a/src/routes/docker-manager.ts +++ b/src/routes/docker-manager.ts @@ -23,6 +23,7 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) { detail: { tags: ["Management"], + description: "Add a new Host as Monitoring target", }, body: t.Object({ name: t.String(), @@ -49,6 +50,7 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) { detail: { tags: ["Management"], + description: "Update an already existing target's config", }, body: t.Object({ name: t.String(), @@ -77,6 +79,7 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) { detail: { tags: ["Management"], + description: "Returns an Array of Host-config-objects", }, }, ); diff --git a/src/routes/docker-stats.ts b/src/routes/docker-stats.ts index d85bfc1..9eb3403 100644 --- a/src/routes/docker-stats.ts +++ b/src/routes/docker-stats.ts @@ -101,6 +101,8 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) { detail: { tags: ["Statistics"], + description: + "Fetches all Containers and their statistics across all Hosts", }, }, ) @@ -152,6 +154,7 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) { detail: { tags: ["Statistics"], + description: "Fetches the Host Stats for a specified Host", }, }, ); diff --git a/src/routes/logs.ts b/src/routes/logs.ts index a8cae1c..f5cf3cb 100644 --- a/src/routes/logs.ts +++ b/src/routes/logs.ts @@ -20,6 +20,7 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) { detail: { tags: ["Management"], + description: "Retrieves all Logs which have been saved in the Database", }, }, ) @@ -41,6 +42,7 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) { detail: { tags: ["Management"], + description: "Retrieves all Logs with the specified level", }, }, ) @@ -62,6 +64,7 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) { detail: { tags: ["Management"], + description: "Deletes all Logs which are saved in the Database", }, }, ) @@ -83,6 +86,8 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) { detail: { tags: ["Management"], + description: + "Deletes all Logs with the specified Level inside the Database", }, }, ); diff --git a/src/routes/stacks.ts b/src/routes/stacks.ts index f4be7b5..c4e6c2c 100644 --- a/src/routes/stacks.ts +++ b/src/routes/stacks.ts @@ -7,6 +7,7 @@ import { restartStack, getStackStatus, startStack, + getAllStacksStatus, } from "~/core/stacks/controller"; import { dbFunctions } from "~/core/database/repository"; import { logger } from "~/core/utils/logger"; @@ -47,23 +48,27 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) body.automatic_reboot_on_error, isCustom, image_updates, - body.stack_prefix + body.stack_prefix, ); logger.info(`Deployed Stack (${body.name})`); return responseHandler.ok( set, - `Stack ${body.name} deployed successfully` + `Stack ${body.name} deployed successfully`, ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error deploying stack" + "Error deploying stack", ); } }, { - detail: { tags: ["Stacks"] }, + detail: { + tags: ["Stacks"], + description: + "Deploy a Stack, either with a prebuilt one or provide your own structure", + }, body: t.Object({ compose_spec: t.Any(), name: t.String(), @@ -74,7 +79,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) source: t.String(), stack_prefix: t.Optional(t.String()), }), - } + }, ) .post( "/start", @@ -87,22 +92,22 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) logger.info(`Started Stack (${body.stack})`); return responseHandler.ok( set, - `Stack ${body.stack} started successfully` + `Stack ${body.stack} started successfully`, ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error starting stack" + "Error starting stack", ); } }, { - detail: { tags: ["Stacks"] }, + detail: { tags: ["Stacks"], description: "Start a specific Stack" }, body: t.Object({ stack: t.Any(), }), - } + }, ) .post( "/stop", @@ -115,22 +120,22 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) logger.info(`Stopped Stack (${body.stack})`); return responseHandler.ok( set, - `Stack ${body.stack} stopped successfully` + `Stack ${body.stack} stopped successfully`, ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error stopping stack" + "Error stopping stack", ); } }, { - detail: { tags: ["Stacks"] }, + detail: { tags: ["Stacks"], description: "Stop the specified Stack" }, body: t.Object({ stack: t.Any(), }), - } + }, ) .post( "/restart", @@ -143,22 +148,22 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) logger.info(`Restarted Stack (${body.stack})`); return responseHandler.ok( set, - `Stack ${body.stack} restarted successfully` + `Stack ${body.stack} restarted successfully`, ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error restarting stack" + "Error restarting stack", ); } }, { - detail: { tags: ["Stacks"] }, + detail: { tags: ["Stacks"], description: "Restart a whole Stack" }, body: t.Object({ stack: t.Any(), }), - } + }, ) .post( "/pull-images", @@ -171,52 +176,63 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) logger.info(`Pulled Stack images (${body.stack})`); return responseHandler.ok( set, - `Images for stack ${body.stack} pulled successfully` + `Images for stack ${body.stack} pulled successfully`, ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error pulling images" + "Error pulling images", ); } }, { - detail: { tags: ["Stacks"] }, + detail: { + tags: ["Stacks"], + description: "Runs `docker compose pull` on the provided Stack", + }, body: t.Object({ stack: t.Any(), }), - } + }, ) .get( "/status", async ({ set, query }) => { try { - if (!query.stack_name) { - throw new Error("Stack needed"); + let status; + let res = {}; + if (query.stack_name) { + status = await getStackStatus(query.stack_name); + res = responseHandler.ok( + set, + `Stack ${query.stack_name} status retrieved successfully`, + ); + logger.info("Fetched Stack status"); + } else { + status = await getAllStacksStatus(); + res = responseHandler.ok(set, "Fetched all Stack's status"); + logger.info("Fetched all Stack status"); } - logger.debug(query.stack_name); - const status = await getStackStatus(query.stack_name); - const res = responseHandler.ok( - set, - `Stack ${query.stack_name} status retrieved successfully` - ); - logger.info("Fetched Stack status"); return { ...res, status: status }; } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error getting stack status" + "Error getting stack status", ); } }, { - detail: { tags: ["Stacks"] }, + detail: { + tags: ["Stacks"], + description: + "Fetches the current status of all containers for a specific Stack or if no Stack name is provided, for all Stacks", + }, query: t.Object({ stack_name: t.Any(), }), - } + }, ) .get( "/", @@ -229,11 +245,14 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) return responseHandler.error( set, error.message || error, - "Error getting stacks" + "Error getting stacks", ); } }, { - detail: { tags: ["Stacks"] }, - } + detail: { + tags: ["Stacks"], + description: "Returns an Array of Stack-config-objects", + }, + }, ); diff --git a/src/typings/database.ts b/src/typings/database.ts index c9b15d3..cebda60 100644 --- a/src/typings/database.ts +++ b/src/typings/database.ts @@ -1,11 +1,3 @@ -interface backend_log_entries { - timestamp: string; - level: string; - message: string; - file: string; - line: number; -} - interface config { keep_data_for: number; fetching_interval: number; @@ -23,4 +15,4 @@ interface stacks_config { image_updates: boolean; } -export type { backend_log_entries, config, stacks_config }; +export type { config, stacks_config }; From 68a6461843c7a39f3b8c7cf5981bcd205b326462 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 19 Mar 2025 12:01:12 +0100 Subject: [PATCH 196/324] Feat: Docker image workflow --- .dockerignore | 6 +++- .github/workflows/docker.yaml | 68 +++++++++++++++++++++++++++++++++++ docker/Dockerfile | 40 ++++++++++++++++++++- package.json | 1 - 4 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/docker.yaml diff --git a/.dockerignore b/.dockerignore index 1e14091..db08b7e 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,8 @@ *.db* /stacks /node_modules -*.md \ No newline at end of file +*.md +/docker +*.dot +*.mmd +*.lock diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml new file mode 100644 index 0000000..bc5690d --- /dev/null +++ b/.github/workflows/docker.yaml @@ -0,0 +1,68 @@ +name: Docker Build and Test Workflow + +on: + push: + branches: ["**"] + release: + types: [published, prereleased] + +jobs: + test: + name: Test Build on Push + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Build image for testing + uses: docker/build-push-action@v4 + with: + context: . + load: true + # build only for current architecture (usually linux/amd64) to enable local loading + platforms: linux/amd64 + tags: dockstatapi:test + + release: + name: Build and Push Docker Image on Release + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GHCR_PAT }} + + - name: Determine image tag + id: tag + run: | + TAG=${GITHUB_REF##*/} + if [ "${{ github.event.release.prerelease }}" = "true" ]; then + TAG="${TAG}-rc" + fi + echo "IMAGE_TAG=$TAG" >> $GITHUB_ENV + echo "Using tag: $TAG" + + - name: Build and push Docker image + uses: docker/build-push-action@v4 + with: + context: . + push: true + platforms: linux/amd64,linux/arm64,linux/arm/v7 + tags: ghcr.io/${{ github.repository_owner }}/dockstatapi:${{ env.IMAGE_TAG }} diff --git a/docker/Dockerfile b/docker/Dockerfile index fdd4234..64b9ba4 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,3 +1,41 @@ -FROM oven/bun AS base +ARG BUILD_DATE +ARG VCS_REF +FROM oven/bun:alpine AS base WORKDIR /base + +COPY package.json bun.lock ./ +RUN bun install -p + +COPY . . + +FROM oven/bun:alpine AS production +WORKDIR /DockStatAPI + +LABEL org.opencontainers.image.title="DockStatAPI" \ + org.opencontainers.image.description="A Dockerized DockStatAPI built with Bun on Alpine Linux." \ + org.opencontainers.image.version="1.0.0" \ + org.opencontainers.image.authors="Your Name " \ + org.opencontainers.image.vendor="Your Company" \ + org.opencontainers.image.licenses="MIT" \ + org.opencontainers.image.created=$BUILD_DATE \ + org.opencontainers.image.revision=$VCS_REF + +RUN apk add --no-cache curl + +HEALTHCHECK --timeout=30s --start-period=5s --retries=3 \ + CMD curl --fail http://localhost:3000/health || exit 1 + +VOLUME [ "/DockStatAPI/src/plugins" ] + +ENV NODE_ENV=production +ENV LOG_LEVEL=info + +EXPOSE 3000 + +COPY --from=base /base /DockStatAPI + +RUN adduser -D DockStatAPI && chown -R DockStatAPI:DockStatAPI /DockStatAPI +USER DockStatAPI + +ENTRYPOINT [ "bun", "run", "src/index.ts" ] diff --git a/package.json b/package.json index 3426d54..c3f1792 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,6 @@ "version": "2.1.0", "scripts": { "start": "cross-env NODE_ENV=production LOG_LEVEL=info bun run src/index.ts", - "start:linux": "NODE_ENV=production LOG_LEVEL=info bun run src/index.ts", "dev": "docker compose -f docker/docker-compose.dev.yaml up -d && cross-env NODE_ENV=dev bun run --watch src/index.ts", "dev:clean": "bun dev ; echo '\nExiting...' ; bun clean", "build": "bun build --target bun src/index.ts --outdir ./dist", From c341826d2c0b5ea31f3a4972c3338c0075ebecea Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 19 Mar 2025 12:06:54 +0100 Subject: [PATCH 197/324] Fix: Adjust Dockerfile location --- .github/workflows/docker.yaml | 2 ++ docker/Dockerfile | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index bc5690d..9fb1724 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -24,6 +24,7 @@ jobs: uses: docker/build-push-action@v4 with: context: . + file: docker/Dockerfile load: true # build only for current architecture (usually linux/amd64) to enable local loading platforms: linux/amd64 @@ -63,6 +64,7 @@ jobs: uses: docker/build-push-action@v4 with: context: . + file: docker/Dockerfile push: true platforms: linux/amd64,linux/arm64,linux/arm/v7 tags: ghcr.io/${{ github.repository_owner }}/dockstatapi:${{ env.IMAGE_TAG }} diff --git a/docker/Dockerfile b/docker/Dockerfile index 64b9ba4..21dbcac 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -4,7 +4,7 @@ ARG VCS_REF FROM oven/bun:alpine AS base WORKDIR /base -COPY package.json bun.lock ./ +COPY package.json ./ RUN bun install -p COPY . . From 1d96562aab612fedc579cd270fa93e20df78a57f Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 19 Mar 2025 12:10:22 +0100 Subject: [PATCH 198/324] Fix: Adjust Login Code --- .github/workflows/docker.yaml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 9fb1724..6d0c3e0 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -12,16 +12,16 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Build image for testing - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v6 with: context: . file: docker/Dockerfile @@ -35,20 +35,20 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to GitHub Container Registry - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} - password: ${{ secrets.GHCR_PAT }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Determine image tag id: tag @@ -61,7 +61,7 @@ jobs: echo "Using tag: $TAG" - name: Build and push Docker image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v6 with: context: . file: docker/Dockerfile From 58b0bd578ba6ffa8bfaeba277737996c6e1a5ed2 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 19 Mar 2025 12:16:13 +0100 Subject: [PATCH 199/324] Fix: Repository name must be lowercase :sob: --- .github/workflows/docker.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 6d0c3e0..259d483 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -67,4 +67,4 @@ jobs: file: docker/Dockerfile push: true platforms: linux/amd64,linux/arm64,linux/arm/v7 - tags: ghcr.io/${{ github.repository_owner }}/dockstatapi:${{ env.IMAGE_TAG }} + tags: ghcr.io/its4nik/dockstatapi:${{ env.IMAGE_TAG }} From d861dc624b36643d6f758aa5dd374346ddf08346 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 19 Mar 2025 12:19:58 +0100 Subject: [PATCH 200/324] Fix: Bun alpine image does not support arm/v7 --- .github/workflows/docker.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 259d483..51f618a 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -66,5 +66,5 @@ jobs: context: . file: docker/Dockerfile push: true - platforms: linux/amd64,linux/arm64,linux/arm/v7 + platforms: linux/amd64,linux/arm64 tags: ghcr.io/its4nik/dockstatapi:${{ env.IMAGE_TAG }} From 2149866caccfb9803453bcf4c0e054cc2897c385 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 19 Mar 2025 12:23:32 +0100 Subject: [PATCH 201/324] Fix: Add permissions --- .github/workflows/docker.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 51f618a..782a2dc 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -6,6 +6,10 @@ on: release: types: [published, prereleased] +permissions: + contents: read + packages: write + jobs: test: name: Test Build on Push From 5015d83fa2b611b818f4045106397a7f21a943f5 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 19 Mar 2025 12:32:20 +0100 Subject: [PATCH 202/324] Fix: Adjustment to Dockerfile --- docker/Dockerfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 21dbcac..da82057 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -14,10 +14,10 @@ WORKDIR /DockStatAPI LABEL org.opencontainers.image.title="DockStatAPI" \ org.opencontainers.image.description="A Dockerized DockStatAPI built with Bun on Alpine Linux." \ - org.opencontainers.image.version="1.0.0" \ - org.opencontainers.image.authors="Your Name " \ - org.opencontainers.image.vendor="Your Company" \ - org.opencontainers.image.licenses="MIT" \ + org.opencontainers.image.version="3.0.0" \ + org.opencontainers.image.authors="info@itsnik.de" \ + org.opencontainers.image.vendor="Its4Nik" \ + org.opencontainers.image.licenses="CC BY-NC 4.0" \ org.opencontainers.image.created=$BUILD_DATE \ org.opencontainers.image.revision=$VCS_REF From d54b56b1b6043ee106cc6a46675eab72af59112b Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 19 Mar 2025 13:26:22 +0100 Subject: [PATCH 203/324] Feat: Info Route --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c3f1792..3b4498a 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "license": "CC BY-NC 4.0", "contributors": [], "description": "DockStatAPI is an API backend featuring plugins and more for DockStat", - "version": "2.1.0", + "version": "3.0.0", "scripts": { "start": "cross-env NODE_ENV=production LOG_LEVEL=info bun run src/index.ts", "dev": "docker compose -f docker/docker-compose.dev.yaml up -d && cross-env NODE_ENV=dev bun run --watch src/index.ts", From ede35bacfbf7a2e753d91e4d558137539253a66d Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 19 Mar 2025 14:50:45 +0100 Subject: [PATCH 204/324] Feat: Add utils route(s) --- src/routes/utils.ts | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/routes/utils.ts diff --git a/src/routes/utils.ts b/src/routes/utils.ts new file mode 100644 index 0000000..3afbf35 --- /dev/null +++ b/src/routes/utils.ts @@ -0,0 +1,45 @@ +import { Elysia, t } from "elysia"; +import { + version, + authorEmail, + authorName, + authorWebsite, + contributors, + dependencies, + description, + devDependencies, + license, +} from "~/core/utils/package-json"; +import { responseHandler } from "~/core/utils/respone-handler"; + +export const utilRoutes = new Elysia({ prefix: "/utils" }).get( + "/info", + async ({ set }) => { + try { + set.status = 200; + return { + version, + authorEmail, + authorName, + authorWebsite, + contributors, + dependencies, + description, + devDependencies, + license, + }; + } catch (error: any) { + return responseHandler.error( + set, + error.message || error, + "Error getting DockStatAPI information", + ); + } + }, + { + detail: { + tags: ["Utils"], + description: "Shows general information about DockStatAPI", + }, + }, +); From b48d6167bda0fa0b869adb49b834a2e004048958 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 21 Mar 2025 01:37:46 +0100 Subject: [PATCH 205/324] Feat: Live Log endpoint and adjustments --- bun.lock | 2 + src/core/utils/logger.ts | 23 ++- src/index.ts | 12 +- src/middleware/auth.ts | 10 +- src/routes/docker-websocket.ts | 261 +++++++++------------------------ src/routes/live-logs.ts | 29 ++++ src/typings/websocket.ts | 12 +- 7 files changed, 147 insertions(+), 202 deletions(-) create mode 100644 src/routes/live-logs.ts diff --git a/bun.lock b/bun.lock index 9a3a82f..b8dfca8 100644 --- a/bun.lock +++ b/bun.lock @@ -388,6 +388,8 @@ "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "color-string/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "defaults/clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], "easy-table/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts index 8c321ad..7e3dd56 100644 --- a/src/core/utils/logger.ts +++ b/src/core/utils/logger.ts @@ -3,6 +3,10 @@ import path from "path"; import chalk, { ChalkInstance } from "chalk"; import { dbFunctions } from "../database/repository"; import wrapAnsi from "wrap-ansi"; +import { logToClients } from "~/routes/live-logs"; +import { logStreamData } from "~/typings/websocket"; + +const ansiRegex = /\x1B\[[0-?9;]*[mG]/g; // Change to false here if dont want the spacing on a wrapped line const padNewlines: boolean = true; @@ -80,6 +84,16 @@ export const logger = createLogger({ message = `[ ${chalk.greenBright("Plugin")} ] ${message}`; } + const logStreamData: logStreamData = { + timestamp: timestamp as string, + level: level as string, + message: (message as string).replace(ansiRegex, ""), + file: file as string, + line: line as number, + }; + + logToClients(logStreamData); + const paddedLevel = level.toUpperCase().padEnd(5); const coloredLevel = (levelColors[level] || chalk.white)(paddedLevel); const coloredContext = chalk.cyan(`${file as string}:${line as number}`); @@ -87,7 +101,7 @@ export const logger = createLogger({ if (process.env.NODE_ENV !== "dev") { return `${coloredLevel} [ ${coloredTimestamp} ] - ${chalk.gray( - message, + message )} - [ ${coloredContext} ]`; } @@ -95,16 +109,15 @@ export const logger = createLogger({ const prefixLength = prefix.length; const formattedMessage = formatTerminalMessage( message as string, - prefixLength, + prefixLength ); - const ansiRegex = /\x1B\[[0-?9;]*[mG]/g; try { dbFunctions.addLogEntry( (level as string).replace(ansiRegex, ""), (message as string).replace(ansiRegex, ""), (file as string).replace(ansiRegex, ""), - line as number, + line as number ); } catch (error) { // Use console.error to avoid recursive logging @@ -113,7 +126,7 @@ export const logger = createLogger({ } return `${coloredLevel} [ ${coloredTimestamp} ] - ${formattedMessage} - [ ${coloredContext} ]`; - }), + }) ), transports: [new transports.Console()], }); diff --git a/src/index.ts b/src/index.ts index ff6b763..ad16d1a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,6 +16,8 @@ import trpcRouter from "~/core/trpc"; import { config } from "./typings/database"; import { validateApiKey } from "./middleware/auth"; import { monitorDockerEvents } from "./core/docker/monitor"; +import { liveLogs } from "./routes/live-logs"; +import { utilRoutes } from "./routes/utils"; console.log(""); dbFunctions.init(); @@ -66,7 +68,7 @@ const DockStatAPI = new Elysia() }, ], }, - }), + }) ) .onBeforeHandle(async (context) => { const { path, request, set } = context; @@ -91,6 +93,8 @@ const DockStatAPI = new Elysia() .use(dockerWebsocketRoutes) .use(apiConfigRoutes) .use(stackRoutes) + .use(utilRoutes) + .use(liveLogs) .get("/health", () => ({ status: "healthy" }), { tags: ["Utils"] }) .onError(({ code, set, path }) => { if (code === "NOT_FOUND") { @@ -115,7 +119,7 @@ async function startServer() { if (apiKey === "changeme") { logger.warn( - "Default API Key of 'changeme' detected. Please change your API Key via the `/config/update` route!", + "Default API Key of 'changeme' detected. Please change your API Key via the `/config/update` route!" ); } @@ -123,10 +127,10 @@ async function startServer() { console.log("----- [ ############## ]"); logger.info(`DockStatAPI is running at http://${hostname}:${port}`); logger.info( - `Swagger API Documentation available at http://${hostname}:${port}/swagger`, + `Swagger API Documentation available at http://${hostname}:${port}/swagger` ); logger.info( - `tRPC Endpoint available at: http://${hostname}:${port}/trpc`, + `tRPC Endpoint available at: http://${hostname}:${port}/trpc` ); }); } catch (error) { diff --git a/src/middleware/auth.ts b/src/middleware/auth.ts index ac0c860..5ec3f19 100644 --- a/src/middleware/auth.ts +++ b/src/middleware/auth.ts @@ -16,7 +16,7 @@ export async function hashApiKey(apiKey: string): Promise { async function validateApiKeyHash( providedKey: string, - storedHash: string, + storedHash: string ): Promise { logger.debug("Validating API key hash"); try { @@ -30,7 +30,7 @@ async function validateApiKeyHash( } async function getApiKeyFromDb( - apiKey: string, + apiKey: string ): Promise<{ hash: string } | null> { const dbApiKey = (dbFunctions.getConfig() as config[])[0].api_key; logger.debug(`Querying database for API key: ${apiKey}`); @@ -41,9 +41,11 @@ async function getApiKeyFromDb( export async function validateApiKey(request: Request, set: set) { const apiKey = request.headers.get("x-api-key"); - logger.debug(`API key validation initiated`); if (process.env.NODE_ENV != "production") { + logger.warn( + "API Key validation deactivated, since running in development mode" + ); return { apiKey }; } else if (!apiKey) { logger.error(`API key missing from request ${request.url}`); @@ -51,6 +53,8 @@ export async function validateApiKey(request: Request, set: set) { return { error: "API key required" }; } + logger.debug(`API key validation initiated`); + try { const dbRecord = await getApiKeyFromDb(apiKey); diff --git a/src/routes/docker-websocket.ts b/src/routes/docker-websocket.ts index 4b4fae7..5520579 100644 --- a/src/routes/docker-websocket.ts +++ b/src/routes/docker-websocket.ts @@ -1,6 +1,5 @@ -import type { StatusMap } from "elysia"; import { Elysia } from "elysia"; -import type { HTTPHeaders } from "elysia/dist/types"; +import type { ElysiaWS } from "elysia/dist/ws"; import { dbFunctions } from "~/core/database/repository"; import { getDockerClient } from "~/core/docker/client"; import { @@ -9,236 +8,122 @@ import { } from "~/core/utils/calculations"; import { logger } from "~/core/utils/logger"; import { responseHandler } from "~/core/utils/respone-handler"; -import type { DockerHost } from "~/typings/docker"; import split2 from "split2"; import type { Readable } from "stream"; -import type { streams } from "~/typings/websocket"; -interface ExtendedWebSocket extends WebSocket { - isOpen: boolean; - streams: any[]; - heartbeat: NodeJS.Timeout | null; -} - -const set: { headers: HTTPHeaders; status?: number | keyof StatusMap } = { - headers: {}, -}; +const activeDockerConnections = new Set>(); +const connectionStreams = new Map< + ElysiaWS, + Array<{ statsStream: Readable; splitStream: ReturnType }> +>(); export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( "/stats", { - async open(socket) { - socket.send(JSON.stringify({ message: "Connection established" })); - let hosts: DockerHost[]; - - (socket as unknown as ExtendedWebSocket).isOpen = true; - (socket as unknown as ExtendedWebSocket).streams = []; - (socket as unknown as ExtendedWebSocket).heartbeat = null; // Add heartbeat reference + async open(ws) { + activeDockerConnections.add(ws); + connectionStreams.set(ws, []); - logger.info(`Opened WebSocket (${socket.id})`); + ws.send(JSON.stringify({ message: "Connection established" })); + logger.info(`New Docker WebSocket established (${ws.id})`); try { - hosts = dbFunctions.getDockerHosts(); - logger.debug( - `Retrieved ${hosts.length} docker host(s) from the database`, - ); - } catch (error: unknown) { - const errResponse = responseHandler.error( - set, - (error as Error).message, - "Failed to retrieve Docker hosts", - 500, - ); - logger.error( - `Error retrieving Docker hosts: ${(error as Error).message}`, - ); - socket.send(JSON.stringify(errResponse)); - return; - } - - // Add heartbeat using WebSocket protocol-level ping - (socket as any).heartbeat = setInterval(() => { - if (!(socket as unknown as ExtendedWebSocket).isOpen) { - clearInterval((socket as any).heartbeat); - return; - } - socket.ping(); // Use WebSocket protocol ping - }, 30000); - - for (const host of hosts) { - if (!(socket as unknown as ExtendedWebSocket).isOpen) { - break; - } + const hosts = dbFunctions.getDockerHosts(); + logger.debug(`Retrieved ${hosts.length} docker host(s)`); - logger.debug(`Processing host: ${host.name}`); + for (const host of hosts) { + if (ws.readyState !== 1) { + break; + } - try { const docker = getDockerClient(host); await docker.ping(); - logger.debug(`Ping successful for host: ${host.name}`); - logger.debug(`Listing containers for host: ${host.name}`); const containers = await docker.listContainers(); - logger.debug( - `Found ${containers.length} container(s) on host ${host.name}`, - ); + logger.debug(`Found ${containers.length} containers on ${host.name}`); for (const containerInfo of containers) { - if (!(socket as unknown as ExtendedWebSocket).isOpen) { + if (ws.readyState !== 1) { break; } - logger.debug( - `Processing container ${containerInfo.Id} on host ${host.name}`, - ); const container = docker.getContainer(containerInfo.Id); - try { - logger.debug( - `Starting stats stream for container ${containerInfo.Id} on host ${host.name}`, - ); - const statsStream = (await container.stats({ - stream: true, - })) as Readable; - const splitStream = split2(); - - // Store both streams for cleanup - (socket as unknown as ExtendedWebSocket).streams.push({ - statsStream, - splitStream, - }); - - // Handle stream lifecycle - statsStream - .on("close", () => { - logger.debug(`Stats stream closed for ${containerInfo.Id}`); - splitStream.destroy(); - }) - .on("end", () => { - logger.debug(`Stats stream ended for ${containerInfo.Id}`); - splitStream.destroy(); - }); - - statsStream - .pipe(splitStream) - .on("data", (line: string) => { - // 1 = OPEN state - if (socket.readyState !== 1) { - return; - } - if (!line) { - return; - } - try { - const stats = JSON.parse(line); - const cpuUsage = calculateCpuPercent(stats); - const memoryUsage = calculateMemoryUsage(stats); - - const data = { + const statsStream = (await container.stats({ + stream: true, + })) as Readable; + const splitStream = split2(); + + connectionStreams.get(ws)?.push({ statsStream, splitStream }); + + statsStream + .on("close", () => splitStream.destroy()) + .pipe(splitStream) + .on("data", (line: string) => { + if (ws.readyState !== 1 || !line) return; + try { + const stats = JSON.parse(line); + ws.send( + JSON.stringify({ id: containerInfo.Id, hostId: host.name, name: containerInfo.Names[0].replace(/^\//, ""), image: containerInfo.Image, status: containerInfo.Status, state: containerInfo.State, - cpuUsage, - memoryUsage, - }; - socket.send(JSON.stringify(data)); - } catch (parseErr: any) { - logger.error( - `Failed to parse stats for container ${containerInfo.Id} on host ${host.name}: ${parseErr.message}`, - ); - } - }) - .on("error", (err: Error) => { - logger.error( - `Stats stream error for container ${containerInfo.Id} on host ${host.name}: ${err.message}`, + cpuUsage: calculateCpuPercent(stats), + memoryUsage: calculateMemoryUsage(stats), + }) ); - if (socket.readyState === 1) { - socket.send( - JSON.stringify({ - hostId: host.name, - containerId: containerInfo.Id, - error: `Stats stream error for container ${containerInfo.Id} on host ${host.name}`, - }), - ); - } - statsStream.destroy(); - }); - } catch (streamErr: any) { - const errMsg = `Failed to start stats stream for container ${containerInfo.Id}`; - logger.error( - `Failed to start stats stream for container ${containerInfo.Id} on host ${host.name}: ${streamErr.message}`, - ); - if (socket.readyState === 1) { - socket.send( + } catch (error) { + logger.error(`Parse error: ${error}`); + } + }) + .on("error", (error: Error) => { + logger.error(`Stream error: ${error}`); + statsStream.destroy(); + ws.send( JSON.stringify({ hostId: host.name, containerId: containerInfo.Id, - error: errMsg, - }), + error: `Stats stream error: ${error}`, + }) ); - } - } - } - } catch (err: any) { - logger.error( - `Failed to list containers for host ${host.name}: ${err.message}`, - ); - const errResponse = responseHandler.error( - set, - err.message, - `Failed to list containers for host ${host.name}`, - 500, - ); - if (socket.readyState === 1) { - socket.send( - JSON.stringify({ - hostId: host.name, - error: errResponse.error, - }), - ); + }); } } + } catch (error) { + logger.error(`Connection error: ${error}`); + ws.send( + JSON.stringify( + responseHandler.error( + { headers: {} }, + error as string, + "Docker connection failed", + 500 + ) + ) + ); } }, - message(_, message) { - if (message === "pong") { - return; - } + message(ws, message) { + if (message === "pong") ws.pong(); }, - close(socket, code, reason) { - logger.info(`Closing SplitStream and WebSocket (${socket.id})`); - const wasOpen = (socket as unknown as ExtendedWebSocket).isOpen; - (socket as unknown as ExtendedWebSocket).isOpen = false; - - // Immediate heartbeat cleanup - clearInterval((socket as any).heartbeat); + close(ws) { + logger.info(`Closing connection ${ws.id}`); + activeDockerConnections.delete(ws); - // Force-close streams using destructor pattern - const streams: streams[] = - (socket as unknown as ExtendedWebSocket).streams || []; + const streams = connectionStreams.get(ws) || []; streams.forEach(({ statsStream, splitStream }) => { try { - // Immediate pipeline breakdown statsStream.unpipe(splitStream); - statsStream.destroy(new Error("WebSocket closed")); - splitStream.destroy(new Error("WebSocket closed")); - - // Remove all potential listeners - statsStream.removeAllListeners(); - splitStream.removeAllListeners(); - } catch (err) { - logger.error(`Stream cleanup error: ${err}`); + statsStream.destroy(); + splitStream.destroy(); + } catch (error) { + logger.error(`Cleanup error: ${error}`); } }); - - if (wasOpen) { - logger.info( - `Closed WebSocket (${socket.id}) - Code: ${code} - Reason: ${reason}`, - ); - } + connectionStreams.delete(ws); }, - }, + } ); diff --git a/src/routes/live-logs.ts b/src/routes/live-logs.ts new file mode 100644 index 0000000..1232ce5 --- /dev/null +++ b/src/routes/live-logs.ts @@ -0,0 +1,29 @@ +import { Elysia } from "elysia"; +import type { ElysiaWS } from "elysia/dist/ws"; +import { logger } from "~/core/utils/logger"; +import type { logStreamData } from "~/typings/websocket"; + +const activeConnections = new Set>(); + +export const liveLogs = new Elysia({ prefix: "/logs" }).ws("/ws", { + open(ws) { + activeConnections.add(ws); + ws.send({ message: "Connection established" }); + logger.info(`New Logs WebSocket established (${ws.id})`); + }, + close(ws) { + logger.info(`Logs WebSocket closed (${ws.id})`); + activeConnections.delete(ws); + }, +}); + +export function logToClients(data: logStreamData) { + activeConnections.forEach((ws) => { + try { + ws.send(JSON.stringify(data)); + } catch (error) { + activeConnections.delete(ws); + logger.error("Failed to send to WebSocket:", error); + } + }); +} diff --git a/src/typings/websocket.ts b/src/typings/websocket.ts index a971247..59f5e3a 100644 --- a/src/typings/websocket.ts +++ b/src/typings/websocket.ts @@ -1,4 +1,4 @@ -import type { Readable } from "stream"; +import type { Readable, Transform } from "stream"; import type internal from "stream"; interface streams { @@ -6,4 +6,12 @@ interface streams { splitStream: internal.Transform; } -export { streams }; +interface logStreamData { + timestamp: string; + level: string; + message: string; + file: string; + line: number; +} + +export { streams, logStreamData }; From 74459313d385ce3c75b79a5ce71190a6a17ab41e Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Fri, 21 Mar 2025 00:38:15 +0000 Subject: [PATCH 206/324] Update dependency graphs --- dependency-graph.dot | 12 +- dependency-graph.mmd | 256 +++++------ dependency-graph.svg | 994 +++++++++++++++++++++++-------------------- 3 files changed, 668 insertions(+), 594 deletions(-) diff --git a/dependency-graph.dot b/dependency-graph.dot index 8759b82..f6d55f9 100644 --- a/dependency-graph.dot +++ b/dependency-graph.dot @@ -104,6 +104,8 @@ strict digraph "dependency-cruiser output"{ "src/core/utils/change-me-checker.ts" -> "fs/promises" [style="dashed" penwidth="1.0"] subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/logger.ts" [label= tooltip="logger.ts" URL="src/core/utils/logger.ts" fillcolor="#ddfeff"] } } } "src/core/utils/logger.ts" -> "src/core/database/repository.ts" [arrowhead="normalnoneodot"] + "src/core/utils/logger.ts" -> "src/routes/live-logs.ts" [arrowhead="normalnoneodot"] + "src/core/utils/logger.ts" -> "src/typings/websocket.ts" "src/core/utils/logger.ts" -> "path" [style="dashed" penwidth="1.0"] subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/package-json.ts" [label= tooltip="package-json.ts" URL="src/core/utils/package-json.ts" fillcolor="#ddfeff"] } } } "src/core/utils/package-json.ts" -> "package.json" @@ -113,7 +115,9 @@ strict digraph "dependency-cruiser output"{ subgraph "cluster_src" {label="src" "src/index.ts" [label= tooltip="index.ts" URL="src/index.ts" fillcolor="#ddfeff"] } "src/index.ts" -> "src/core/docker/monitor.ts" "src/index.ts" -> "src/middleware/auth.ts" + "src/index.ts" -> "src/routes/live-logs.ts" "src/index.ts" -> "src/routes/stacks.ts" + "src/index.ts" -> "src/routes/utils.ts" "src/index.ts" -> "src/typings/database.ts" "src/index.ts" -> "src/core/database/repository.ts" "src/index.ts" -> "src/core/docker/scheduler.ts" @@ -156,9 +160,10 @@ strict digraph "dependency-cruiser output"{ "src/routes/docker-websocket.ts" -> "src/core/utils/calculations.ts" "src/routes/docker-websocket.ts" -> "src/core/utils/logger.ts" "src/routes/docker-websocket.ts" -> "src/core/utils/respone-handler.ts" - "src/routes/docker-websocket.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] - "src/routes/docker-websocket.ts" -> "src/typings/websocket.ts" [arrowhead="onormal" penwidth="1.0"] "src/routes/docker-websocket.ts" -> "stream" [style="dashed" penwidth="1.0" arrowhead="onormal"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/live-logs.ts" [label= tooltip="live-logs.ts" URL="src/routes/live-logs.ts" fillcolor="#ddfeff"] } } + "src/routes/live-logs.ts" -> "src/core/utils/logger.ts" [arrowhead="normalnoneodot"] + "src/routes/live-logs.ts" -> "src/typings/websocket.ts" [arrowhead="onormal" penwidth="1.0"] subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/logs.ts" [label= tooltip="logs.ts" URL="src/routes/logs.ts" fillcolor="#ddfeff"] } } "src/routes/logs.ts" -> "src/core/database/repository.ts" "src/routes/logs.ts" -> "src/core/utils/logger.ts" @@ -167,6 +172,9 @@ strict digraph "dependency-cruiser output"{ "src/routes/stacks.ts" -> "src/core/stacks/controller.ts" "src/routes/stacks.ts" -> "src/core/utils/logger.ts" "src/routes/stacks.ts" -> "src/core/utils/respone-handler.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/utils.ts" [label= tooltip="utils.ts" URL="src/routes/utils.ts" fillcolor="#ddfeff"] } } + "src/routes/utils.ts" -> "src/core/utils/package-json.ts" + "src/routes/utils.ts" -> "src/core/utils/respone-handler.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/database.ts" [label= tooltip="database.ts" URL="src/typings/database.ts" fillcolor="#ddfeff"] } } subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/docker-compose.ts" [label= tooltip="docker-compose.ts" URL="src/typings/docker-compose.ts" fillcolor="#ddfeff"] } } subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/docker.ts" [label= tooltip="docker.ts" URL="src/typings/docker.ts" fillcolor="#ddfeff"] } } diff --git a/dependency-graph.mmd b/dependency-graph.mmd index 0e9f516..0197483 100644 --- a/dependency-graph.mmd +++ b/dependency-graph.mmd @@ -11,98 +11,104 @@ subgraph 0["src"] subgraph 2["core"] subgraph 3["docker"] 4["monitor.ts"] -K["client.ts"] -U["scheduler.ts"] -V["store-host-stats.ts"] -X["store-container-stats.ts"] +O["client.ts"] +10["scheduler.ts"] +11["store-host-stats.ts"] +13["store-container-stats.ts"] end subgraph 6["plugins"] 7["plugin-manager.ts"] -Z["loader.ts"] +15["loader.ts"] end subgraph 9["utils"] A["logger.ts"] -T["respone-handler.ts"] -Y["calculations.ts"] -11["change-me-checker.ts"] -19["package-json.ts"] +W["respone-handler.ts"] +Y["package-json.ts"] +14["calculations.ts"] +17["change-me-checker.ts"] end subgraph C["database"] D["repository.ts"] F["helper.ts"] end -subgraph Q["stacks"] -R["controller.ts"] +subgraph T["stacks"] +U["controller.ts"] end -subgraph 13["trpc"] -14["index.ts"] -15["router.ts"] -subgraph 16["procedures"] -17["api-config.procedure.ts"] -1B["docker-manager.procedure.ts"] -1C["docker-stats.procedure.ts"] -1D["logs.procedure.ts"] -1E["stacks.procedure.ts"] +subgraph 19["trpc"] +1A["index.ts"] +1B["router.ts"] +subgraph 1C["procedures"] +1D["api-config.procedure.ts"] +1F["docker-manager.procedure.ts"] +1G["docker-stats.procedure.ts"] +1H["logs.procedure.ts"] +1I["stacks.procedure.ts"] end -18["trpc.ts"] +1E["trpc.ts"] end end subgraph G["typings"] H["database.ts"] I["docker.ts"] -J["plugin.ts"] -N["elysiajs.ts"] -S["docker-compose.ts"] -W["dockerode.ts"] -1K["websocket.ts"] +L["websocket.ts"] +N["plugin.ts"] +R["elysiajs.ts"] +V["docker-compose.ts"] +12["dockerode.ts"] end -subgraph L["middleware"] -M["auth.ts"] +subgraph J["routes"] +K["live-logs.ts"] +S["stacks.ts"] +X["utils.ts"] +1J["api-config.ts"] +1K["docker-manager.ts"] +1L["docker-stats.ts"] +1M["docker-websocket.ts"] +1N["logs.ts"] end -subgraph O["routes"] -P["stacks.ts"] -1F["api-config.ts"] -1G["docker-manager.ts"] -1H["docker-stats.ts"] -1I["docker-websocket.ts"] -1L["logs.ts"] +subgraph P["middleware"] +Q["auth.ts"] end end 5["bun"] 8["events"] B["path"] E["bun:sqlite"] -subgraph 10["fs"] -12["promises"] +M["stream"] +Z["package.json"] +subgraph 16["fs"] +18["promises"] end -1A["package.json"] -1J["stream"] 1-->4 -1-->M -1-->P +1-->Q +1-->K +1-->S +1-->X 1-->H 1-->D -1-->U -1-->Z -1-->14 +1-->10 +1-->15 +1-->1A 1-->A -1-->1F -1-->1G -1-->1H -1-->1I +1-->1J +1-->1K 1-->1L +1-->1M +1-->1N 4-->7 4-->D -4-->K +4-->O 4-->A 4-->I 4-->I 4-->5 7-->A 7-->I -7-->J +7-->N 7-->8 A-->D +A-->K +A-->L A-->B D-->F D-->A @@ -110,99 +116,101 @@ D-->H D-->I D-->E F-->A -J-->I K-->A -K-->I -M-->D -M-->A -M-->H -M-->N -P-->D -P-->R -P-->A -P-->T -R-->D -R-->A -R-->H -R-->S -T-->A -T-->N +K-->L +L-->M +N-->I +O-->A +O-->I +Q-->D +Q-->A +Q-->H +Q-->R +S-->D +S-->U +S-->A +S-->W U-->D -U-->V -U-->X U-->A U-->H -V-->D -V-->K -V-->A -V-->I -V-->W -X-->D -X-->K +U-->V +W-->A +W-->R X-->Y -Z-->11 -Z-->A -Z-->7 -Z-->10 -Z-->B +X-->W +Y-->Z +10-->D +10-->11 +10-->13 +10-->A +10-->H +11-->D +11-->O 11-->A +11-->I 11-->12 -14-->15 +13-->D +13-->O +13-->14 15-->17 -15-->1B -15-->1C -15-->1D -15-->1E -15-->18 -17-->18 -17-->D +15-->A +15-->7 +15-->16 +15-->B 17-->A -17-->19 -17-->H -19-->1A -1B-->18 -1B-->D -1B-->A -1C-->18 -1C-->D -1C-->K -1C-->Y -1C-->A -1C-->I -1C-->W -1D-->18 +17-->18 +1A-->1B +1B-->1D +1B-->1F +1B-->1G +1B-->1H +1B-->1I +1B-->1E +1D-->1E 1D-->D 1D-->A -1E-->18 -1E-->D -1E-->R -1E-->A +1D-->Y +1D-->H +1F-->1E 1F-->D -1F-->7 1F-->A -1F-->19 -1F-->T -1F-->M -1F-->H +1G-->1E 1G-->D +1G-->O +1G-->14 1G-->A -1G-->T +1G-->I +1G-->12 +1H-->1E 1H-->D -1H-->K -1H-->Y 1H-->A -1H-->T -1H-->I -1H-->W +1I-->1E 1I-->D -1I-->K -1I-->Y +1I-->U 1I-->A -1I-->T -1I-->I -1I-->1K -1I-->1J -1K-->1J +1J-->D +1J-->7 +1J-->A +1J-->Y +1J-->W +1J-->Q +1J-->H +1K-->D +1K-->A +1K-->W 1L-->D +1L-->O +1L-->14 1L-->A +1L-->W +1L-->I +1L-->12 +1M-->D +1M-->O +1M-->14 +1M-->A +1M-->W +1M-->M +1N-->D +1N-->A diff --git a/dependency-graph.svg b/dependency-graph.svg index 7ea0c59..89342e8 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -4,70 +4,70 @@ - - + + dependency-cruiser output - + cluster_fs - -fs + +fs cluster_src - -src + +src cluster_src/core - -core + +core cluster_src/core/database - -database + +database cluster_src/core/docker - -docker + +docker cluster_src/core/plugins - -plugins + +plugins cluster_src/core/stacks - -stacks + +stacks cluster_src/core/trpc - -trpc + +trpc cluster_src/core/trpc/procedures - -procedures + +procedures cluster_src/core/utils - -utils + +utils cluster_src/middleware - -middleware + +middleware cluster_src/routes - -routes + +routes cluster_src/typings @@ -78,8 +78,8 @@ bun - -bun + +bun @@ -87,8 +87,8 @@ bun:sqlite - -bun:sqlite + +bun:sqlite @@ -96,8 +96,8 @@ events - -events + +events @@ -105,8 +105,8 @@ fs - -fs + +fs @@ -114,8 +114,8 @@ fs/promises - -promises + +promises @@ -123,8 +123,8 @@ package.json - -package.json + +package.json @@ -132,8 +132,8 @@ path - -path + +path @@ -141,8 +141,8 @@ src/core/database/helper.ts - -helper.ts + +helper.ts @@ -150,63 +150,95 @@ src/core/utils/logger.ts - -logger.ts + +logger.ts src/core/database/helper.ts->src/core/utils/logger.ts - - - - + + + + - + src/core/utils/logger.ts->path - - + + src/core/database/repository.ts - -repository.ts + +repository.ts src/core/utils/logger.ts->src/core/database/repository.ts - - - - + + + + + + + +src/routes/live-logs.ts + + +live-logs.ts + + + + + +src/core/utils/logger.ts->src/routes/live-logs.ts + + + + + + + +src/typings/websocket.ts + + +websocket.ts + + + + + +src/core/utils/logger.ts->src/typings/websocket.ts + + src/core/database/repository.ts->bun:sqlite - - + + src/core/database/repository.ts->src/core/database/helper.ts - - - - + + + + src/core/database/repository.ts->src/core/utils/logger.ts - - - - + + + + @@ -220,8 +252,8 @@ src/core/database/repository.ts->src/typings/database.ts - - + + @@ -235,197 +267,197 @@ src/core/database/repository.ts->src/typings/docker.ts - - + + src/core/docker/client.ts - -client.ts + +client.ts src/core/docker/client.ts->src/core/utils/logger.ts - - + + src/core/docker/client.ts->src/typings/docker.ts - - + + src/core/docker/monitor.ts - -monitor.ts + +monitor.ts src/core/docker/monitor.ts->bun - - + + src/core/docker/monitor.ts->src/core/utils/logger.ts - - + + src/core/docker/monitor.ts->src/core/database/repository.ts - - + + src/core/docker/monitor.ts->src/typings/docker.ts - - + + src/core/docker/monitor.ts->src/core/docker/client.ts - - + + src/core/plugins/plugin-manager.ts - -plugin-manager.ts + +plugin-manager.ts src/core/docker/monitor.ts->src/core/plugins/plugin-manager.ts - - + + src/core/plugins/plugin-manager.ts->events - - + + src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts - - + + src/core/plugins/plugin-manager.ts->src/typings/docker.ts - - + + src/typings/plugin.ts - -plugin.ts + +plugin.ts src/core/plugins/plugin-manager.ts->src/typings/plugin.ts - - + + src/core/docker/scheduler.ts - -scheduler.ts + +scheduler.ts src/core/docker/scheduler.ts->src/core/utils/logger.ts - - + + src/core/docker/scheduler.ts->src/core/database/repository.ts - - + + src/core/docker/scheduler.ts->src/typings/database.ts - + src/core/docker/store-host-stats.ts - -store-host-stats.ts + +store-host-stats.ts src/core/docker/scheduler.ts->src/core/docker/store-host-stats.ts - - + + src/core/docker/store-container-stats.ts - -store-container-stats.ts + +store-container-stats.ts src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts - - + + src/core/docker/store-host-stats.ts->src/core/utils/logger.ts - - + + src/core/docker/store-host-stats.ts->src/core/database/repository.ts - - + + src/core/docker/store-host-stats.ts->src/typings/docker.ts - - + + src/core/docker/store-host-stats.ts->src/core/docker/client.ts - - + + @@ -439,127 +471,127 @@ src/core/docker/store-host-stats.ts->src/typings/dockerode.ts - - + + src/core/docker/store-container-stats.ts->src/core/database/repository.ts - - + + src/core/docker/store-container-stats.ts->src/core/docker/client.ts - - + + src/core/utils/calculations.ts - -calculations.ts + +calculations.ts src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts - - + + src/core/plugins/loader.ts - -loader.ts + +loader.ts src/core/plugins/loader.ts->fs - - + + src/core/plugins/loader.ts->path - - + + src/core/plugins/loader.ts->src/core/utils/logger.ts - - + + src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts - - + + src/core/utils/change-me-checker.ts - -change-me-checker.ts + +change-me-checker.ts src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts - - + + src/core/utils/change-me-checker.ts->fs/promises - - + + src/core/utils/change-me-checker.ts->src/core/utils/logger.ts - - + + - + src/typings/plugin.ts->src/typings/docker.ts - - + + src/core/stacks/controller.ts - -controller.ts + +controller.ts src/core/stacks/controller.ts->src/core/utils/logger.ts - - + + src/core/stacks/controller.ts->src/core/database/repository.ts - - + + src/core/stacks/controller.ts->src/typings/database.ts - + @@ -574,15 +606,15 @@ src/core/stacks/controller.ts->src/typings/docker-compose.ts - - + + src/core/trpc/index.ts - -index.ts + +index.ts @@ -590,673 +622,699 @@ src/core/trpc/router.ts - -router.ts + +router.ts src/core/trpc/index.ts->src/core/trpc/router.ts - - + + src/core/trpc/procedures/api-config.procedure.ts - -api-config.procedure.ts + +api-config.procedure.ts src/core/trpc/router.ts->src/core/trpc/procedures/api-config.procedure.ts - - + + src/core/trpc/trpc.ts - -trpc.ts + +trpc.ts src/core/trpc/router.ts->src/core/trpc/trpc.ts - - + + src/core/trpc/procedures/docker-manager.procedure.ts - -docker-manager.procedure.ts + +docker-manager.procedure.ts src/core/trpc/router.ts->src/core/trpc/procedures/docker-manager.procedure.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts - -docker-stats.procedure.ts + +docker-stats.procedure.ts src/core/trpc/router.ts->src/core/trpc/procedures/docker-stats.procedure.ts - - + + src/core/trpc/procedures/logs.procedure.ts - -logs.procedure.ts + +logs.procedure.ts src/core/trpc/router.ts->src/core/trpc/procedures/logs.procedure.ts - - + + src/core/trpc/procedures/stacks.procedure.ts - -stacks.procedure.ts + +stacks.procedure.ts src/core/trpc/router.ts->src/core/trpc/procedures/stacks.procedure.ts - - + + src/core/trpc/procedures/api-config.procedure.ts->src/core/utils/logger.ts - - + + src/core/trpc/procedures/api-config.procedure.ts->src/core/database/repository.ts - - + + src/core/trpc/procedures/api-config.procedure.ts->src/typings/database.ts - - + + src/core/trpc/procedures/api-config.procedure.ts->src/core/trpc/trpc.ts - - + + src/core/utils/package-json.ts - -package-json.ts + +package-json.ts src/core/trpc/procedures/api-config.procedure.ts->src/core/utils/package-json.ts - - + + - + src/core/utils/package-json.ts->package.json - - + + src/core/trpc/procedures/docker-manager.procedure.ts->src/core/utils/logger.ts - - + + src/core/trpc/procedures/docker-manager.procedure.ts->src/core/database/repository.ts - - + + src/core/trpc/procedures/docker-manager.procedure.ts->src/core/trpc/trpc.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/utils/logger.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/database/repository.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/docker.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/docker/client.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/utils/calculations.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/dockerode.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/trpc/trpc.ts - - + + src/core/trpc/procedures/logs.procedure.ts->src/core/utils/logger.ts - - + + src/core/trpc/procedures/logs.procedure.ts->src/core/database/repository.ts - - + + src/core/trpc/procedures/logs.procedure.ts->src/core/trpc/trpc.ts - - + + src/core/trpc/procedures/stacks.procedure.ts->src/core/utils/logger.ts - - + + src/core/trpc/procedures/stacks.procedure.ts->src/core/database/repository.ts - - + + src/core/trpc/procedures/stacks.procedure.ts->src/core/stacks/controller.ts - - + + src/core/trpc/procedures/stacks.procedure.ts->src/core/trpc/trpc.ts - - + + + + + +src/routes/live-logs.ts->src/core/utils/logger.ts + + + + + + + +src/routes/live-logs.ts->src/typings/websocket.ts + + + + + +stream + + +stream + + + + + +src/typings/websocket.ts->stream + + - + src/core/utils/respone-handler.ts - - -respone-handler.ts + + +respone-handler.ts - + src/core/utils/respone-handler.ts->src/core/utils/logger.ts - - + + - + src/typings/elysiajs.ts - - -elysiajs.ts + + +elysiajs.ts - + src/core/utils/respone-handler.ts->src/typings/elysiajs.ts - - + + - + src/index.ts - - -index.ts + + +index.ts - + src/index.ts->src/core/utils/logger.ts - - + + - + src/index.ts->src/core/database/repository.ts - - + + - + src/index.ts->src/typings/database.ts - + - + src/index.ts->src/core/docker/monitor.ts - - + + - + src/index.ts->src/core/docker/scheduler.ts - - + + - + src/index.ts->src/core/plugins/loader.ts - - + + - + src/index.ts->src/core/trpc/index.ts - - + + + + + +src/index.ts->src/routes/live-logs.ts + + - + src/middleware/auth.ts - - -auth.ts + + +auth.ts - + src/index.ts->src/middleware/auth.ts - - + + - + src/routes/stacks.ts - - -stacks.ts + + +stacks.ts - + src/index.ts->src/routes/stacks.ts - - + + + + + +src/routes/utils.ts + + +utils.ts + + + + + +src/index.ts->src/routes/utils.ts + + - + src/routes/api-config.ts - - -api-config.ts + + +api-config.ts - + src/index.ts->src/routes/api-config.ts - - + + - + src/routes/docker-manager.ts - - -docker-manager.ts + + +docker-manager.ts - + src/index.ts->src/routes/docker-manager.ts - - + + - + src/routes/docker-stats.ts - - -docker-stats.ts + + +docker-stats.ts - + src/index.ts->src/routes/docker-stats.ts - - + + - + src/routes/docker-websocket.ts - - -docker-websocket.ts + + +docker-websocket.ts - + src/index.ts->src/routes/docker-websocket.ts - - + + - + src/routes/logs.ts - - -logs.ts + + +logs.ts - + src/index.ts->src/routes/logs.ts - - + + - + src/middleware/auth.ts->src/core/utils/logger.ts - - + + - + src/middleware/auth.ts->src/core/database/repository.ts - - + + - + src/middleware/auth.ts->src/typings/database.ts - + - + src/middleware/auth.ts->src/typings/elysiajs.ts - - + + - + src/routes/stacks.ts->src/core/utils/logger.ts - - + + - + src/routes/stacks.ts->src/core/database/repository.ts - - + + - + src/routes/stacks.ts->src/core/stacks/controller.ts - - + + - + src/routes/stacks.ts->src/core/utils/respone-handler.ts - - + + + + + +src/routes/utils.ts->src/core/utils/package-json.ts + + + + + +src/routes/utils.ts->src/core/utils/respone-handler.ts + + - + src/routes/api-config.ts->src/core/utils/logger.ts - - + + - + src/routes/api-config.ts->src/core/database/repository.ts - - + + - + src/routes/api-config.ts->src/typings/database.ts - - + + - + src/routes/api-config.ts->src/core/plugins/plugin-manager.ts - - + + - + src/routes/api-config.ts->src/core/utils/package-json.ts - - + + - + src/routes/api-config.ts->src/core/utils/respone-handler.ts - - + + - + src/routes/api-config.ts->src/middleware/auth.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-manager.ts->src/core/database/repository.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/respone-handler.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-stats.ts->src/core/database/repository.ts - - + + - + src/routes/docker-stats.ts->src/typings/docker.ts - - + + - + src/routes/docker-stats.ts->src/core/docker/client.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-stats.ts->src/typings/dockerode.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/respone-handler.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-websocket.ts->src/core/database/repository.ts - - - - - -src/routes/docker-websocket.ts->src/typings/docker.ts - - + + - + src/routes/docker-websocket.ts->src/core/docker/client.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/respone-handler.ts - - - - - -src/typings/websocket.ts - - -websocket.ts - - - - - -src/routes/docker-websocket.ts->src/typings/websocket.ts - - - - - -stream - - -stream - - + + - + src/routes/docker-websocket.ts->stream - - + + - + src/routes/logs.ts->src/core/utils/logger.ts - - + + - + src/routes/logs.ts->src/core/database/repository.ts - - - - - -src/typings/websocket.ts->stream - - + + From 81add7460fa79e5bdc9906b0e3951d6928d1a39c Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 21 Mar 2025 01:48:22 +0100 Subject: [PATCH 207/324] Feat: Unit testing --- package.json | 1 + src/core/utils/logger.ts | 18 ++++-- src/index.ts | 3 +- src/tests/cleanup.ts | 7 +++ src/tests/delete.spec.ts | 12 ++++ src/tests/gets.spec.ts | 59 +++++++++++++++++++ src/tests/helper.ts | 119 +++++++++++++++++++++++++++++++++++++++ src/tests/post.spec.ts | 40 +++++++++++++ 8 files changed, 254 insertions(+), 5 deletions(-) create mode 100644 src/tests/cleanup.ts create mode 100644 src/tests/delete.spec.ts create mode 100644 src/tests/gets.spec.ts create mode 100644 src/tests/helper.ts create mode 100644 src/tests/post.spec.ts diff --git a/package.json b/package.json index 3b4498a..3964ec8 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "description": "DockStatAPI is an API backend featuring plugins and more for DockStat", "version": "3.0.0", "scripts": { + "test": "bun clean && bun test && bun run ./src/tests/cleanup.ts", "start": "cross-env NODE_ENV=production LOG_LEVEL=info bun run src/index.ts", "dev": "docker compose -f docker/docker-compose.dev.yaml up -d && cross-env NODE_ENV=dev bun run --watch src/index.ts", "dev:clean": "bun dev ; echo '\nExiting...' ; bun clean", diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts index 7e3dd56..903df1f 100644 --- a/src/core/utils/logger.ts +++ b/src/core/utils/logger.ts @@ -9,7 +9,7 @@ import { logStreamData } from "~/typings/websocket"; const ansiRegex = /\x1B\[[0-?9;]*[mG]/g; // Change to false here if dont want the spacing on a wrapped line -const padNewlines: boolean = true; +const padNewlines: boolean = process.env.PAD_NEW_LINES === "true" || true; const fileLineFormat = format((info) => { try { @@ -69,6 +69,7 @@ export const logger = createLogger({ verbose: chalk.cyan.bold, silly: chalk.magenta.bold, task: chalk.cyan.bold, + ut: chalk.hex("#9D00FF"), }; if ((message as string).startsWith("__task__")) { @@ -80,6 +81,11 @@ export const logger = createLogger({ } } + if ((message as string).startsWith("__UT__")) { + message = (message as string).replaceAll("__UT__", "").trimStart(); + level = "ut"; + } + if ((file as string).includes("plugin.ts")) { message = `[ ${chalk.greenBright("Plugin")} ] ${message}`; } @@ -101,7 +107,7 @@ export const logger = createLogger({ if (process.env.NODE_ENV !== "dev") { return `${coloredLevel} [ ${coloredTimestamp} ] - ${chalk.gray( - message + message, )} - [ ${coloredContext} ]`; } @@ -117,7 +123,7 @@ export const logger = createLogger({ (level as string).replace(ansiRegex, ""), (message as string).replace(ansiRegex, ""), (file as string).replace(ansiRegex, ""), - line as number + line as number, ); } catch (error) { // Use console.error to avoid recursive logging @@ -126,7 +132,11 @@ export const logger = createLogger({ } return `${coloredLevel} [ ${coloredTimestamp} ] - ${formattedMessage} - [ ${coloredContext} ]`; - }) + + const fullMessage = `${coloredLevel} [ ${coloredTimestamp} ] - ${message} - [ ${coloredContext} ]`; + + return formatTerminalMessage(fullMessage, prefixLength); + }), ), transports: [new transports.Console()], }); diff --git a/src/index.ts b/src/index.ts index ad16d1a..f96da4b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,7 +22,7 @@ import { utilRoutes } from "./routes/utils"; console.log(""); dbFunctions.init(); -const DockStatAPI = new Elysia() +export const DockStatAPI = new Elysia() .use(staticPlugin()) .use(serverTiming()) .use( @@ -92,6 +92,7 @@ const DockStatAPI = new Elysia() .use(backendLogs) .use(dockerWebsocketRoutes) .use(apiConfigRoutes) + .use(utilRoutes) .use(stackRoutes) .use(utilRoutes) .use(liveLogs) diff --git a/src/tests/cleanup.ts b/src/tests/cleanup.ts new file mode 100644 index 0000000..6965dd4 --- /dev/null +++ b/src/tests/cleanup.ts @@ -0,0 +1,7 @@ +import { dbFunctions } from "~/core/database/repository"; + +console.log(""); +console.log("Deleting `test` Docker host"); +dbFunctions.deleteDockerHost("test"); +console.log("Cleanuing up Database config to default values"); +dbFunctions.updateConfig(5, 7, "changeme"); diff --git a/src/tests/delete.spec.ts b/src/tests/delete.spec.ts new file mode 100644 index 0000000..ee8ad22 --- /dev/null +++ b/src/tests/delete.spec.ts @@ -0,0 +1,12 @@ +import { describe, it } from "bun:test"; +import { runTestCode } from "./helper"; + +describe("DockStatAPI (DELETE)", () => { + it("Delete all Logs /logs", async () => { + await runTestCode("/logs", 200, "DELETE", "{}"); + }); + + it("Delete Logs (Debug) /logs/debug", async () => { + await runTestCode("/logs/debug", 200, "DELETE", "{}"); + }); +}); diff --git a/src/tests/gets.spec.ts b/src/tests/gets.spec.ts new file mode 100644 index 0000000..f211c39 --- /dev/null +++ b/src/tests/gets.spec.ts @@ -0,0 +1,59 @@ +import { describe, it } from "bun:test"; +import { runTestResponse, runTestCode } from "./helper"; +import { + version, + authorEmail, + authorName, + authorWebsite, + contributors, + dependencies, + description, + devDependencies, + license, +} from "~/core/utils/package-json"; + +describe("DockStatAPI (GET)", () => { + it("Check Server connection", async () => { + await runTestResponse("/health", '{"status":"healthy"}', "GET"); + }); + + it("Check /docker/containers", async () => { + await runTestCode("/docker/containers", 200, "GET"); + }); + + it("Check /docker/hosts/Localhost", async () => { + await runTestCode("/docker/hosts/Localhost", 200, "GET"); + }); + + it("Check /docker-config/hosts", async () => { + await runTestCode("/docker-config/hosts", 200, "GET"); + }); + + it("Check /logs/", async () => { + await runTestCode("/logs", 200, "GET"); + }); + + it("Check /logs/debug", async () => { + await runTestCode("/logs/debug", 200, "GET"); + }); + + it("Check /config", async () => { + await runTestCode("/config", 200, "GET"); + }); + + it("Check /config/package", async () => { + const expected = { + version, + description, + license, + authorName, + authorEmail, + authorWebsite, + contributors, + dependencies, + devDependencies, + }; + + await runTestResponse("/config/package", JSON.stringify(expected), "GET"); + }); +}); diff --git a/src/tests/helper.ts b/src/tests/helper.ts new file mode 100644 index 0000000..1c773df --- /dev/null +++ b/src/tests/helper.ts @@ -0,0 +1,119 @@ +import { expect } from "bun:test"; +import { DockStatAPI } from ".."; +import { logger } from "~/core/utils/logger"; +export const API_KEY = "TestKey"; + +export async function runTestResponse( + path: string, + expected_response: string, + method?: "GET" | "POST" | "DELETE", +) { + if (!method) { + method = "GET"; + } + + const server = "http://localhost:3000"; + const route = `${server}${path}`; + + logger.info(`__UT__ [START] Running test, method: ${method} on ${route}`); + const startTime = Date.now(); + + try { + const request = new Request(route, { + method, + verbose: true, + headers: { + "Content-Type": "application/json", + "x-api-key": API_KEY, + }, + }); + logger.debug( + `__UT__ Request details: ${JSON.stringify({ + url: route, + method, + headers: [...request.headers], + })}`, + ); + + // Get the response + const response = await DockStatAPI.handle(request); + const headers: any = {}; + response.headers.forEach((value, key: any) => { + headers[key] = value; + }); + logger.debug(`__UT__ Received HTTP status: ${response.status}`); + logger.debug(`__UT__ Response headers: ${JSON.stringify(headers)}`); + + // Log the response body as text + const responseText = await response.text(); + const duration = Date.now() - startTime; + logger.debug(`__UT__ Response body: ${responseText}`); + logger.debug(`__UT__ Total Duration: ${duration}ms`); + logger.info(`__UT__ [END] Completed test on ${route}`); + + return expect(responseText).toBe(expected_response); + } catch (error) { + logger.error(`__UT__ Error during test on ${route}: ${error}`); + throw error; + } +} + +export async function runTestCode( + path: string, + expected_code: number, + method?: "GET" | "POST" | "DELETE", + requestBody?: string, +) { + if (!method) { + method = "GET"; + } + + if (!requestBody) { + requestBody = ""; + } + + const server = "http://localhost:3000"; + const route = `${server}${path}`; + + logger.info(`__UT__ [START] Running test, method: ${method} on ${route}`); + const startTime = Date.now(); + + try { + const request = new Request(route, { + method, + verbose: true, + body: requestBody, + headers: { + "Content-Type": "application/json", + "x-api-key": API_KEY, + }, + }); + logger.debug( + `__UT__ Request details: ${JSON.stringify({ + url: route, + method, + headers: [...request.headers], + body: requestBody, + })}`, + ); + + const response = await DockStatAPI.handle(request); + logger.debug(`__UT__ Received HTTP status: ${response.status}`); + + const headers: any = {}; + response.headers.forEach((value, key) => { + headers[key] = value; + }); + + logger.debug(`__UT__ Response headers: ${JSON.stringify(headers)}`); + logger.debug(`__UT__ Response: ${JSON.stringify(response.body)}`); + + const duration = Date.now() - startTime; + logger.debug(`__UT__ Completed test on ${route} (Duration: ${duration}ms)`); + + expect(response.status).toBe(expected_code); + } catch (error) { + logger.error(`__UT__ Error during test on ${route}: ${error}`); + throw error; + } +} diff --git a/src/tests/post.spec.ts b/src/tests/post.spec.ts new file mode 100644 index 0000000..4d780ee --- /dev/null +++ b/src/tests/post.spec.ts @@ -0,0 +1,40 @@ +import { describe, it } from "bun:test"; +import { runTestResponse, runTestCode } from "./helper"; +import { dbFunctions } from "~/core/database/repository"; +import { API_KEY } from "./helper"; + +describe("DockStatAPI (POST)", () => { + it("Check Host adding", async () => { + const body: string = + '{"name":"test","url":"localhost:2375","secure":false}'; + + await runTestCode("/docker-config/add-host", 200, "POST", body); + await runTestResponse( + "/docker-config/hosts", + '[{"name":"test","url":"localhost:2375","secure":0},{"name":"Localhost","url":"localhost:2375","secure":0}]', + "GET", + ); + }); + + it("Check Host Updating", async () => { + const body: string = + '{"name":"test","url":"127.0.0.1:2375","secure":false}'; + + await runTestCode("/docker-config/update-host", 200, "POST", body); + await runTestResponse( + "/docker-config/hosts", + '[{"name":"test","url":"127.0.0.1:2375","secure":0},{"name":"Localhost","url":"localhost:2375","secure":0}]', + "GET", + ); + }); + + it("Check Config update", async () => { + const body = `{"fetching_interval":"1","keep_data_for":"1","api_key":${API_KEY}}`; + await runTestCode( + "/config/update", + 200, + "POST", + '{"fetching_interval":"1","keep_data_for":"1","api_key":"123"}', + ); + }); +}); From 20c29c4e73a14176511121b6a1a4c7bfd93a3a6b Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 21 Mar 2025 19:39:51 +0100 Subject: [PATCH 208/324] Feat: Unit tests and general adjustments --- .dockerignore | 3 + .github/workflows/docker.yaml | 31 +++-- out | 18 +++ package.json | 3 +- src/core/database/repository.ts | 117 +++++++++--------- src/core/docker/client.ts | 6 +- src/core/docker/monitor.ts | 10 +- src/core/docker/store-container-stats.ts | 24 ++-- .../procedures/docker-manager.procedure.ts | 16 +-- src/core/utils/helpers.ts | 15 +++ src/routes/docker-manager.ts | 26 ++-- src/routes/docker-stats.ts | 26 ++-- src/routes/docker-websocket.ts | 6 +- src/routes/utils.ts | 4 +- src/tests/cleanup.ts | 17 ++- src/tests/delete.spec.ts | 4 +- src/tests/gets.spec.ts | 6 +- src/tests/helper.ts | 98 +++++++-------- src/tests/post.spec.ts | 52 ++++---- src/typings/docker.ts | 5 +- 20 files changed, 280 insertions(+), 207 deletions(-) create mode 100644 out create mode 100644 src/core/utils/helpers.ts diff --git a/.dockerignore b/.dockerignore index db08b7e..295585f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,3 +6,6 @@ *.dot *.mmd *.lock +src/tests +.github +.local-tests \ No newline at end of file diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 782a2dc..90e231b 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -15,24 +15,29 @@ jobs: name: Test Build on Push runs-on: ubuntu-latest steps: + - uses: oven-sh/setup-bun@v2 + name: Setup Bun + with: + bun-version: latest + - name: Checkout repository uses: actions/checkout@v4 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + - name: Run Unit-tests + run: | + bun install + bun clean + bun test - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + - name: Run Docker Build + run: | + bun build:docker - - name: Build image for testing - uses: docker/build-push-action@v6 - with: - context: . - file: docker/Dockerfile - load: true - # build only for current architecture (usually linux/amd64) to enable local loading - platforms: linux/amd64 - tags: dockstatapi:test + - name: Start docker container + run: | + docker run --name dockstatapi --rm -d dockstatapi:local + sleep 10 + if [[ $(docker container ls | grep "Up" | wc -l) -gt 0 ]]; then docker kill dockstatapi && exit 0; else; exit 1; fi release: name: Build and Push Docker Image on Release diff --git a/out b/out new file mode 100644 index 0000000..8ca3fef --- /dev/null +++ b/out @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/package.json b/package.json index 3964ec8..3a1dd22 100644 --- a/package.json +++ b/package.json @@ -10,11 +10,12 @@ "description": "DockStatAPI is an API backend featuring plugins and more for DockStat", "version": "3.0.0", "scripts": { - "test": "bun clean && bun test && bun run ./src/tests/cleanup.ts", "start": "cross-env NODE_ENV=production LOG_LEVEL=info bun run src/index.ts", + "start:docker": "bun run build:docker && docker run -p 3001:3000 --rm -d --name dockstatapi -v 'plugins:/DockStatAPI/src/plugins' dockstatapi:local", "dev": "docker compose -f docker/docker-compose.dev.yaml up -d && cross-env NODE_ENV=dev bun run --watch src/index.ts", "dev:clean": "bun dev ; echo '\nExiting...' ; bun clean", "build": "bun build --target bun src/index.ts --outdir ./dist", + "build:docker": "docker build -f docker/Dockerfile . -t 'dockstatapi:local'", "clean": "bun run clean:win || bun run clean:lin", "clean:win": "node -e \"process.exit(process.platform === 'win32' ? 0 : 1)\" && cmd /c del /Q dockstatapi.db* && echo 'success'", "clean:lin": "node -e \"process.exit(process.platform !== 'win32' ? 0 : 1)\" && rm -f dockstatapi.db* && echo 'success'", diff --git a/src/core/database/repository.ts b/src/core/database/repository.ts index 552b6a2..96ab533 100644 --- a/src/core/database/repository.ts +++ b/src/core/database/repository.ts @@ -31,8 +31,9 @@ export const dbFunctions = { ); CREATE TABLE IF NOT EXISTS docker_hosts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, - url TEXT NOT NULL, + hostadress TEXT NOT NULL, secure BOOLEAN NOT NULL ); @@ -87,20 +88,20 @@ export const dbFunctions = { const stmt = db.prepare( ` INSERT INTO config (keep_data_for, fetching_interval, api_key) VALUES (7, 5, "changeme") - `, + ` ); stmt.run(); } const hostRow = db - .prepare(`SELECT COUNT(*) AS count FROM docker_hosts WHERE name = ?`) + .prepare(`SELECT COUNT(*) AS count FROM docker_hosts`) .get("Localhost") as { count: number }; if (hostRow.count === 0) { logger.debug("Initializing default docker host (Localhost)"); const stmt = db.prepare( ` - INSERT INTO docker_hosts (name, url, secure) VALUES (?, ?, ?) - `, + INSERT INTO docker_hosts (name, hostadress, secure) VALUES (?, ?, ?) + ` ); stmt.run("Localhost", "localhost:2375", false); } @@ -109,40 +110,36 @@ export const dbFunctions = { logger.debug(`__task__ __db__ Initializing Database ✔️ (${duration}ms)`); }, - addDockerHost(hostId: string, url: string, secure: boolean) { + addDockerHost(host: DockerHost) { return executeDbOperation( "Add Docker Host", () => { const stmt = db.prepare(` - INSERT INTO docker_hosts (name, url, secure) + INSERT INTO docker_hosts (name, hostadress, secure) VALUES (?, ?, ?) `); - return stmt.run(hostId, url, secure); + return stmt.run(host.name, host.hostadress, host.secure); }, () => { - if (hostId.length < 1) { + if (host.name.length < 1) { logger.error("Hostname needed"); - throw new Error( - "Invalid data provided, please see server's log for more info", - ); + throw new Error("Invalid data provided - Hostname needed"); } - if (url.length < 1) { - logger.error("URL needed"); - throw new Error( - "Invalid data provided, please see server's log for more info", - ); + if (host.hostadress.length < 1) { + logger.error("Hostadress needed"); + throw new Error("Invalid data provided - Hostadress needed"); } if ( - typeof hostId !== "string" || - typeof url !== "string" || - typeof secure !== "boolean" + typeof host.name !== "string" || + typeof host.secure !== "boolean" || + typeof host.hostadress !== "string" ) { logger.error("Invalid parameter types for addDockerHost"); throw new TypeError("Invalid parameter types for addDockerHost"); } - }, + } ); }, @@ -151,13 +148,13 @@ export const dbFunctions = { "Get Docker Hosts", () => { const stmt = db.prepare(` - SELECT name, url, secure + SELECT id, name, hostadress, secure FROM docker_hosts - ORDER BY name DESC + ORDER BY id DESC `); return stmt.all() as DockerHost[]; }, - () => {}, + () => {} ); }, @@ -165,7 +162,7 @@ export const dbFunctions = { level: string, message: string, file_name: string, - line: number, + line: number ) => { if ( typeof level !== "string" || @@ -195,7 +192,7 @@ export const dbFunctions = { `); return stmt.all(); }, - () => {}, + () => {} ); }, @@ -216,50 +213,56 @@ export const dbFunctions = { logger.error("Level parameter must be a string"); throw new TypeError("Level parameter must be a string"); } - }, + } ); }, - updateDockerHost(name: string, url: string, secure: boolean) { + updateDockerHost(host: DockerHost) { return executeDbOperation( "Update Docker Host", () => { const stmt = db.prepare(` UPDATE docker_hosts - SET url = ?, secure = ? - WHERE name = ? + SET hostadress = ?, secure = ?, name = ? + WHERE id = ? `); - return stmt.run(url, secure, name); + return stmt.run( + host.hostadress, + host.secure, + host.name, + String(host.id) + ); }, () => { if ( - typeof name !== "string" || - typeof url !== "string" || - typeof secure !== "boolean" + typeof host.name !== "string" || + typeof host.hostadress !== "string" || + typeof host.secure !== "boolean" || + typeof host.id !== "number" ) { logger.error("Invalid parameter types for updateDockerHost"); throw new TypeError("Invalid parameter types for updateDockerHost"); } - }, + } ); }, - deleteDockerHost(name: string) { + deleteDockerHost(id: number) { return executeDbOperation( "Delete Docker Host", () => { const stmt = db.prepare(` DELETE FROM docker_hosts - WHERE name = ? + WHERE id = ? `); - return stmt.run(name); + return stmt.run(id); }, () => { - if (typeof name !== "string") { + if (typeof id !== "number") { logger.error("Invalid parameter type for deleteDockerHost"); throw new TypeError("Name parameter must be a string"); } - }, + } ); }, @@ -272,7 +275,7 @@ export const dbFunctions = { `); return stmt.run(); }, - () => {}, + () => {} ); }, @@ -291,14 +294,14 @@ export const dbFunctions = { logger.error("Invalid parameter type for clearLogsByLevel"); throw new TypeError("Level parameter must be a string"); } - }, + } ); }, updateConfig( fetching_interval: number, keep_data_for: number, - api_key: string, + api_key: string ) { return executeDbOperation( "Update Config", @@ -319,7 +322,7 @@ export const dbFunctions = { logger.error("Invalid parameter types for updateConfig"); throw new TypeError("Invalid parameter types for updateConfig"); } - }, + } ); }, @@ -333,7 +336,7 @@ export const dbFunctions = { `); return stmt.all(); }, - () => {}, + () => {} ); }, @@ -358,7 +361,7 @@ export const dbFunctions = { logger.error("Invalid parameter type for deleteOldData"); throw new TypeError("Days parameter must be a number"); } - }, + } ); }, @@ -370,7 +373,7 @@ export const dbFunctions = { status: string, state: string, cpu_usage: number, - memory_usage: number, + memory_usage: number ) { return executeDbOperation( "Add Container Stats", @@ -387,7 +390,7 @@ export const dbFunctions = { status, state, cpu_usage, - memory_usage, + memory_usage ); }, () => { @@ -404,7 +407,7 @@ export const dbFunctions = { logger.error("Invalid parameter types for addContainerStats"); throw new TypeError("Invalid parameter types for addContainerStats"); } - }, + } ); }, @@ -457,10 +460,10 @@ export const dbFunctions = { stats.containersRunning, stats.containersStopped, stats.containersPaused, - stats.images, + stats.images ); }, - () => {}, + () => {} ); }, @@ -489,10 +492,10 @@ export const dbFunctions = { stack_config.container_count, stack_config.stack_prefix, stack_config.automatic_reboot_on_error, - stack_config.image_updates, + stack_config.image_updates ); }, - () => {}, + () => {} ); }, @@ -507,7 +510,7 @@ export const dbFunctions = { `); return stmt.all(); }, - () => {}, + () => {} ); }, @@ -521,7 +524,7 @@ export const dbFunctions = { `); return stmt.run(name); }, - () => {}, + () => {} ); }, @@ -549,10 +552,10 @@ export const dbFunctions = { stack_config.stack_prefix, stack_config.automatic_reboot_on_error, stack_config.image_updates, - stack_config.name, + stack_config.name ); }, - () => {}, + () => {} ); }, }; diff --git a/src/core/docker/client.ts b/src/core/docker/client.ts index 010a2bd..324178b 100644 --- a/src/core/docker/client.ts +++ b/src/core/docker/client.ts @@ -4,9 +4,9 @@ import { logger } from "~/core/utils/logger"; export const getDockerClient = (host: DockerHost): Docker => { try { - const inputUrl = host.url.includes("://") - ? host.url - : `${host.secure ? "https" : "http"}://${host.url}`; + const inputUrl = host.hostadress.includes("://") + ? host.hostadress + : `${host.secure ? "https" : "http"}://${host.hostadress}`; const parsedUrl = new URL(inputUrl); const hostAddress = parsedUrl.hostname; let port = parsedUrl.port diff --git a/src/core/docker/monitor.ts b/src/core/docker/monitor.ts index 4f56b2b..1257326 100644 --- a/src/core/docker/monitor.ts +++ b/src/core/docker/monitor.ts @@ -3,7 +3,7 @@ import { dbFunctions } from "~/core/database/repository"; import { getDockerClient } from "~/core/docker/client"; import { logger } from "~/core/utils/logger"; import { pluginManager } from "../plugins/plugin-manager"; -import { HostStats, ContainerInfo } from "~/typings/docker"; +import { ContainerInfo } from "~/typings/docker"; import { sleep } from "bun"; export async function monitorDockerEvents() { @@ -12,7 +12,7 @@ export async function monitorDockerEvents() { try { hosts = dbFunctions.getDockerHosts(); logger.debug( - `Retrieved ${hosts.length} Docker host(s) for event monitoring.`, + `Retrieved ${hosts.length} Docker host(s) for event monitoring.` ); } catch (error: unknown) { logger.error(`Error retrieving Docker hosts: ${(error as Error).message}`); @@ -58,7 +58,7 @@ async function startFor(host: DockerHost) { event = JSON.parse(line); } catch (parseErr: any) { logger.error( - `Failed to parse event from host ${host.name}: ${parseErr.message}`, + `Failed to parse event from host ${host.name}: ${parseErr.message}` ); continue; } @@ -113,7 +113,7 @@ async function startFor(host: DockerHost) { break; default: logger.debug( - `Unhandled container event "${action}" on host ${host.name}`, + `Unhandled container event "${action}" on host ${host.name}` ); } } @@ -132,7 +132,7 @@ async function startFor(host: DockerHost) { }); } catch (streamErr: any) { logger.error( - `Failed to start events stream for host ${host.name}: ${streamErr.message}`, + `Failed to start events stream for host ${host.name}: ${streamErr.message}` ); } } diff --git a/src/core/docker/store-container-stats.ts b/src/core/docker/store-container-stats.ts index e64f31b..6bcc168 100644 --- a/src/core/docker/store-container-stats.ts +++ b/src/core/docker/store-container-stats.ts @@ -5,10 +5,12 @@ import { calculateCpuPercent, calculateMemoryUsage, } from "~/core/utils/calculations"; +import { logger } from "../utils/logger"; async function storeContainerData() { try { const hosts = dbFunctions.getDockerHosts(); + logger.debug("Retrieved docker hosts for storring container data"); // Process each host concurrently and wait for them all to finish await Promise.all( @@ -21,7 +23,7 @@ async function storeContainerData() { } catch (error) { const errMsg = error instanceof Error ? error.message : String(error); throw new Error( - `Failed to ping docker host "${host.name}": ${errMsg}`, + `Failed to ping docker host "${host.name}": ${errMsg}` ); } @@ -31,7 +33,7 @@ async function storeContainerData() { } catch (error) { const errMsg = error instanceof Error ? error.message : String(error); throw new Error( - `Failed to list containers on host "${host.name}": ${errMsg}`, + `Failed to list containers on host "${host.name}": ${errMsg}` ); } @@ -50,20 +52,20 @@ async function storeContainerData() { error instanceof Error ? error.message : String(error); return reject( new Error( - `Failed to get stats for container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}": ${errMsg}`, - ), + `Failed to get stats for container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}": ${errMsg}` + ) ); } if (!stats) { return reject( new Error( - `No stats returned for container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}".`, - ), + `No stats returned for container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}".` + ) ); } resolve(stats); }); - }, + } ); dbFunctions.addContainerStats( @@ -74,18 +76,18 @@ async function storeContainerData() { containerInfo.Status, containerInfo.State, calculateCpuPercent(stats), - calculateMemoryUsage(stats), + calculateMemoryUsage(stats) ); } catch (error) { const errMsg = error instanceof Error ? error.message : String(error); throw new Error( - `Error processing container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}": ${errMsg}`, + `Error processing container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}": ${errMsg}` ); } - }), + }) ); - }), + }) ); } catch (error) { const errMsg = error instanceof Error ? error.message : String(error); diff --git a/src/core/trpc/procedures/docker-manager.procedure.ts b/src/core/trpc/procedures/docker-manager.procedure.ts index 958b31b..93b6a97 100644 --- a/src/core/trpc/procedures/docker-manager.procedure.ts +++ b/src/core/trpc/procedures/docker-manager.procedure.ts @@ -3,26 +3,26 @@ import { logger } from "~/core/utils/logger"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { router, publicProcedure } from "../trpc"; +import { DockerHost } from "~/typings/docker"; const addHostInput = z.object({ name: z.string(), - url: z.string(), + hostadress: z.string(), secure: z.boolean(), }); const updateHostInput = z.object({ name: z.string(), - url: z.string(), + hostadress: z.string(), secure: z.boolean(), }); export const dockerManagerProcedure = router({ addHost: publicProcedure.input(addHostInput).mutation(({ input }) => { try { - const { name, url, secure } = input; - dbFunctions.addDockerHost(name, url, secure); - logger.debug(`Added docker host (${name})`); - return { success: true, message: `Added docker host (${name})` }; + dbFunctions.addDockerHost(input as DockerHost); + logger.debug(`Added docker host (${input.name})`); + return { success: true, message: `Added docker host (${input.name})` }; } catch (error) { logger.error("Error adding docker host", error); throw new TRPCError({ @@ -35,8 +35,8 @@ export const dockerManagerProcedure = router({ updateHost: publicProcedure.input(updateHostInput).mutation(({ input }) => { try { - const { name, url, secure } = input; - dbFunctions.updateDockerHost(name, url, secure); + (input as unknown as DockerHost).id = "0"; + dbFunctions.updateDockerHost(input as DockerHost); return { success: true, message: `Updated docker host (${name})` }; } catch (error) { logger.error("Error updating docker host", error); diff --git a/src/core/utils/helpers.ts b/src/core/utils/helpers.ts new file mode 100644 index 0000000..1bc0206 --- /dev/null +++ b/src/core/utils/helpers.ts @@ -0,0 +1,15 @@ +import { logger } from "./logger"; + +export function findObjectByKey( + array: T[], + key: keyof T, + value: T[keyof T] +): T | undefined { + const data = array.find((item) => item[key] === value); + logger.debug( + `Searching ${String(key)} = ${String(value)} in ${String( + JSON.stringify(array) + )} Found Item ${JSON.stringify(data)}` + ); + return data; +} diff --git a/src/routes/docker-manager.ts b/src/routes/docker-manager.ts index 1d35101..6db7362 100644 --- a/src/routes/docker-manager.ts +++ b/src/routes/docker-manager.ts @@ -8,15 +8,14 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) "/add-host", async ({ set, body }) => { try { - const { name, url, secure } = body; set.headers["Content-Type"] = "application/json"; - dbFunctions.addDockerHost(name, url, secure); - return responseHandler.ok(set, `Added docker host (${name})`); + dbFunctions.addDockerHost(body); + return responseHandler.ok(set, `Added docker host (${body.name})`); } catch (error: unknown) { return responseHandler.error( set, "Error adding docker Host", - error as string, + error as string ); } }, @@ -27,23 +26,23 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) }, body: t.Object({ name: t.String(), - url: t.String(), + hostadress: t.String(), secure: t.Boolean(), }), - }, + } ) .post( "/update-host", async ({ set, body }) => { try { - const { name, url, secure } = body; - dbFunctions.updateDockerHost(name, url, secure); + set.status = 200; + return dbFunctions.updateDockerHost(body); } catch (error) { return responseHandler.error( set, error as string, - "Failed to update host", + "Failed to update host" ); } }, @@ -53,11 +52,12 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) description: "Update an already existing target's config", }, body: t.Object({ + id: t.Number(), name: t.String(), - url: t.String(), + hostadress: t.String(), secure: t.Boolean(), }), - }, + } ) .get( @@ -72,7 +72,7 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) return responseHandler.error( set, error as string, - "Failed to retrieve hosts", + "Failed to retrieve hosts" ); } }, @@ -81,5 +81,5 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) tags: ["Management"], description: "Returns an Array of Host-config-objects", }, - }, + } ); diff --git a/src/routes/docker-stats.ts b/src/routes/docker-stats.ts index 9eb3403..4324fef 100644 --- a/src/routes/docker-stats.ts +++ b/src/routes/docker-stats.ts @@ -29,7 +29,7 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) return responseHandler.error( set, pingError as string, - "Docker host connection failed", + "Docker host connection failed" ); } @@ -47,24 +47,24 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) set, reject, "An error occurred", - error, + error ); } if (!stats) { return responseHandler.reject( set, reject, - "No stats available", + "No stats available" ); } resolve(stats); }); - }, + } ); containers.push({ id: containerInfo.Id, - hostId: host.name, + hostId: host.id as string, name: containerInfo.Names[0].replace(/^\//, ""), image: containerInfo.Image, status: containerInfo.Status, @@ -75,16 +75,16 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) } catch (containerError) { logger.error( "Error fetching container stats,", - containerError, + containerError ); } - }), + }) ); logger.debug(`Fetched stats for ${host.name}`); } catch (hostError) { logger.error("Error fetching containers for host,", hostError); } - }), + }) ); set.headers["Content-Type"] = "application/json"; @@ -94,7 +94,7 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) return responseHandler.error( set, error as string, - "Failed to retrieve containers", + "Failed to retrieve containers" ); } }, @@ -104,7 +104,7 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) description: "Fetches all Containers and their statistics across all Hosts", }, - }, + } ) .get( @@ -117,7 +117,7 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) if (!host) { return responseHandler.simple_error( set, - `Host (${params.id}) not found`, + `Host (${params.id}) not found` ); } @@ -147,7 +147,7 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) return responseHandler.error( set, error as string, - "Failed to retrieve host config", + "Failed to retrieve host config" ); } }, @@ -156,5 +156,5 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) tags: ["Statistics"], description: "Fetches the Host Stats for a specified Host", }, - }, + } ); diff --git a/src/routes/docker-websocket.ts b/src/routes/docker-websocket.ts index 5520579..bd9571b 100644 --- a/src/routes/docker-websocket.ts +++ b/src/routes/docker-websocket.ts @@ -39,7 +39,9 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( const docker = getDockerClient(host); await docker.ping(); const containers = await docker.listContainers(); - logger.debug(`Found ${containers.length} containers on ${host.name}`); + logger.debug( + `Found ${containers.length} containers on ${host.name} (id: ${host.id})` + ); for (const containerInfo of containers) { if (ws.readyState !== 1) { @@ -64,7 +66,7 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( ws.send( JSON.stringify({ id: containerInfo.Id, - hostId: host.name, + hostId: host.id as string, name: containerInfo.Names[0].replace(/^\//, ""), image: containerInfo.Image, status: containerInfo.Status, diff --git a/src/routes/utils.ts b/src/routes/utils.ts index 3afbf35..2253424 100644 --- a/src/routes/utils.ts +++ b/src/routes/utils.ts @@ -32,7 +32,7 @@ export const utilRoutes = new Elysia({ prefix: "/utils" }).get( return responseHandler.error( set, error.message || error, - "Error getting DockStatAPI information", + "Error getting DockStatAPI information" ); } }, @@ -41,5 +41,5 @@ export const utilRoutes = new Elysia({ prefix: "/utils" }).get( tags: ["Utils"], description: "Shows general information about DockStatAPI", }, - }, + } ); diff --git a/src/tests/cleanup.ts b/src/tests/cleanup.ts index 6965dd4..163419a 100644 --- a/src/tests/cleanup.ts +++ b/src/tests/cleanup.ts @@ -1,7 +1,20 @@ import { dbFunctions } from "~/core/database/repository"; +import type { DockerHost } from "~/typings/docker"; +import { findObjectByKey } from "~/core/utils/helpers"; console.log(""); console.log("Deleting `test` Docker host"); -dbFunctions.deleteDockerHost("test"); -console.log("Cleanuing up Database config to default values"); + +let testHosts: DockerHost[] = dbFunctions.getDockerHosts(); + +const testHost = findObjectByKey(testHosts, "name", "test"); + +if (testHost) { + dbFunctions.deleteDockerHost(testHost.id as number); + console.log(`Docker host with name "${testHost.name}" deleted.`); +} else { + console.log("Docker host not found."); +} + +console.log("Cleaning up Database config to default values"); dbFunctions.updateConfig(5, 7, "changeme"); diff --git a/src/tests/delete.spec.ts b/src/tests/delete.spec.ts index ee8ad22..37e36fc 100644 --- a/src/tests/delete.spec.ts +++ b/src/tests/delete.spec.ts @@ -3,10 +3,10 @@ import { runTestCode } from "./helper"; describe("DockStatAPI (DELETE)", () => { it("Delete all Logs /logs", async () => { - await runTestCode("/logs", 200, "DELETE", "{}"); + await runTestCode("/logs", 200, "DELETE", {}); }); it("Delete Logs (Debug) /logs/debug", async () => { - await runTestCode("/logs/debug", 200, "DELETE", "{}"); + await runTestCode("/logs/debug", 200, "DELETE", {}); }); }); diff --git a/src/tests/gets.spec.ts b/src/tests/gets.spec.ts index f211c39..2723508 100644 --- a/src/tests/gets.spec.ts +++ b/src/tests/gets.spec.ts @@ -42,7 +42,7 @@ describe("DockStatAPI (GET)", () => { }); it("Check /config/package", async () => { - const expected = { + const expected = JSON.stringify({ version, description, license, @@ -52,8 +52,8 @@ describe("DockStatAPI (GET)", () => { contributors, dependencies, devDependencies, - }; + }); - await runTestResponse("/config/package", JSON.stringify(expected), "GET"); + await runTestResponse("/config/package", expected, "GET"); }); }); diff --git a/src/tests/helper.ts b/src/tests/helper.ts index 1c773df..bd03055 100644 --- a/src/tests/helper.ts +++ b/src/tests/helper.ts @@ -1,57 +1,62 @@ import { expect } from "bun:test"; import { DockStatAPI } from ".."; import { logger } from "~/core/utils/logger"; + export const API_KEY = "TestKey"; +const server = "http://localhost:3001"; export async function runTestResponse( path: string, - expected_response: string, + expected_response: any, method?: "GET" | "POST" | "DELETE", + requestBody?: any ) { - if (!method) { - method = "GET"; - } - - const server = "http://localhost:3000"; + method = method || "GET"; const route = `${server}${path}`; - logger.info(`__UT__ [START] Running test, method: ${method} on ${route}`); + logger.info(`__UT__ [ START ] Running test, method: ${method} on ${route}`); const startTime = Date.now(); try { + const processedBody = + requestBody !== undefined + ? typeof requestBody === "string" + ? requestBody + : JSON.stringify(requestBody) + : undefined; + const request = new Request(route, { method, - verbose: true, + body: processedBody, headers: { "Content-Type": "application/json", "x-api-key": API_KEY, }, }); + logger.debug( - `__UT__ Request details: ${JSON.stringify({ + `Request details: ${JSON.stringify({ url: route, method, headers: [...request.headers], - })}`, + body: processedBody, + })}` ); - // Get the response const response = await DockStatAPI.handle(request); - const headers: any = {}; - response.headers.forEach((value, key: any) => { - headers[key] = value; - }); - logger.debug(`__UT__ Received HTTP status: ${response.status}`); - logger.debug(`__UT__ Response headers: ${JSON.stringify(headers)}`); + const headers: { [key: string]: string } = {}; + response.headers.forEach((value, key) => (headers[key] = value)); - // Log the response body as text const responseText = await response.text(); const duration = Date.now() - startTime; - logger.debug(`__UT__ Response body: ${responseText}`); - logger.debug(`__UT__ Total Duration: ${duration}ms`); - logger.info(`__UT__ [END] Completed test on ${route}`); - return expect(responseText).toBe(expected_response); + logger.debug(`Received HTTP status: ${response.status}`); + logger.debug(`Response headers: ${JSON.stringify(headers)}`); + logger.debug(`Response body: ${responseText}`); + logger.debug(`Total Duration: ${duration}ms`); + + expect(responseText).toBe(expected_response); + logger.info(`__UT__ [ END ] Completed test on ${route}`); } catch (error) { logger.error(`__UT__ Error during test on ${route}: ${error}`); throw error; @@ -62,56 +67,51 @@ export async function runTestCode( path: string, expected_code: number, method?: "GET" | "POST" | "DELETE", - requestBody?: string, + requestBody?: any ) { - if (!method) { - method = "GET"; - } - - if (!requestBody) { - requestBody = ""; - } - - const server = "http://localhost:3000"; + method = method || "GET"; const route = `${server}${path}`; - logger.info(`__UT__ [START] Running test, method: ${method} on ${route}`); + logger.info(`__UT__ [ START ] Running test, method: ${method} on ${route}`); const startTime = Date.now(); try { + const processedBody = + requestBody !== undefined + ? typeof requestBody === "string" + ? requestBody + : JSON.stringify(requestBody) + : undefined; + const request = new Request(route, { method, - verbose: true, - body: requestBody, + body: processedBody, headers: { "Content-Type": "application/json", "x-api-key": API_KEY, }, }); + logger.debug( - `__UT__ Request details: ${JSON.stringify({ + `Request details: ${JSON.stringify({ url: route, method, headers: [...request.headers], - body: requestBody, - })}`, + body: processedBody, + })}` ); const response = await DockStatAPI.handle(request); - logger.debug(`__UT__ Received HTTP status: ${response.status}`); - - const headers: any = {}; - response.headers.forEach((value, key) => { - headers[key] = value; - }); - - logger.debug(`__UT__ Response headers: ${JSON.stringify(headers)}`); - logger.debug(`__UT__ Response: ${JSON.stringify(response.body)}`); - + const headers: { [key: string]: string } = {}; + response.headers.forEach((value, key) => (headers[key] = value)); const duration = Date.now() - startTime; - logger.debug(`__UT__ Completed test on ${route} (Duration: ${duration}ms)`); + + logger.debug(`Received HTTP status: ${response.status}`); + logger.debug(`Response headers: ${JSON.stringify(headers)}`); + logger.debug(`Response body: ${JSON.stringify(response.body)}`); expect(response.status).toBe(expected_code); + logger.debug(`__UT__ Completed test on ${route} (Duration: ${duration}ms)`); } catch (error) { logger.error(`__UT__ Error during test on ${route}: ${error}`); throw error; diff --git a/src/tests/post.spec.ts b/src/tests/post.spec.ts index 4d780ee..f581770 100644 --- a/src/tests/post.spec.ts +++ b/src/tests/post.spec.ts @@ -1,40 +1,50 @@ import { describe, it } from "bun:test"; import { runTestResponse, runTestCode } from "./helper"; -import { dbFunctions } from "~/core/database/repository"; -import { API_KEY } from "./helper"; +import { DockerHost } from "~/typings/docker"; describe("DockStatAPI (POST)", () => { it("Check Host adding", async () => { - const body: string = - '{"name":"test","url":"localhost:2375","secure":false}'; + const body = { + name: "test", + hostadress: "localhost:2375", + secure: false, + }; await runTestCode("/docker-config/add-host", 200, "POST", body); - await runTestResponse( - "/docker-config/hosts", - '[{"name":"test","url":"localhost:2375","secure":0},{"name":"Localhost","url":"localhost:2375","secure":0}]', - "GET", - ); + await runTestCode("/docker-config/hosts", 200, "GET"); }); it("Check Host Updating", async () => { - const body: string = - '{"name":"test","url":"127.0.0.1:2375","secure":false}'; + const codeBody: DockerHost = { + id: 2, + name: "test", + hostadress: "127.0.0.1:2375", + secure: false, + }; - await runTestCode("/docker-config/update-host", 200, "POST", body); + await runTestCode("/docker-config/update-host", 200, "POST", codeBody); + + const responseBody: DockerHost[] = [ + { id: 2, name: "test", hostadress: "127.0.0.1:2375", secure: 0 }, + { + id: 1, + name: "Localhost", + hostadress: "localhost:2375", + secure: 0, + }, + ]; await runTestResponse( "/docker-config/hosts", - '[{"name":"test","url":"127.0.0.1:2375","secure":0},{"name":"Localhost","url":"localhost:2375","secure":0}]', - "GET", + JSON.stringify(responseBody), + "GET" ); }); it("Check Config update", async () => { - const body = `{"fetching_interval":"1","keep_data_for":"1","api_key":${API_KEY}}`; - await runTestCode( - "/config/update", - 200, - "POST", - '{"fetching_interval":"1","keep_data_for":"1","api_key":"123"}', - ); + await runTestCode("/config/update", 200, "POST", { + fetching_interval: 1, + keep_data_for: 1, + api_key: "TestKey", + }); }); }); diff --git a/src/typings/docker.ts b/src/typings/docker.ts index 522762c..f295ab3 100644 --- a/src/typings/docker.ts +++ b/src/typings/docker.ts @@ -1,7 +1,8 @@ interface DockerHost { name: string; - url: string; - secure: boolean; + hostadress: string; + secure: boolean | number; + id?: number; } interface ContainerInfo { From 2aae57c7fc04503c0da6326ad9374742957f91ae Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Fri, 21 Mar 2025 18:40:21 +0000 Subject: [PATCH 209/324] Update dependency graphs --- dependency-graph.dot | 2 + dependency-graph.mmd | 2 + dependency-graph.svg | 1044 +++++++++++++++++++++--------------------- 3 files changed, 532 insertions(+), 516 deletions(-) diff --git a/dependency-graph.dot b/dependency-graph.dot index f6d55f9..48a4a93 100644 --- a/dependency-graph.dot +++ b/dependency-graph.dot @@ -36,6 +36,7 @@ strict digraph "dependency-cruiser output"{ "src/core/docker/scheduler.ts" -> "src/core/utils/logger.ts" "src/core/docker/scheduler.ts" -> "src/typings/database.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/store-container-stats.ts" [label= tooltip="store-container-stats.ts" URL="src/core/docker/store-container-stats.ts" fillcolor="#ddfeff"] } } } + "src/core/docker/store-container-stats.ts" -> "src/core/utils/logger.ts" "src/core/docker/store-container-stats.ts" -> "src/core/database/repository.ts" "src/core/docker/store-container-stats.ts" -> "src/core/docker/client.ts" "src/core/docker/store-container-stats.ts" -> "src/core/utils/calculations.ts" @@ -73,6 +74,7 @@ strict digraph "dependency-cruiser output"{ "src/core/trpc/procedures/docker-manager.procedure.ts" -> "src/core/trpc/trpc.ts" "src/core/trpc/procedures/docker-manager.procedure.ts" -> "src/core/database/repository.ts" "src/core/trpc/procedures/docker-manager.procedure.ts" -> "src/core/utils/logger.ts" + "src/core/trpc/procedures/docker-manager.procedure.ts" -> "src/typings/docker.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/docker-stats.procedure.ts" [label= tooltip="docker-stats.procedure.ts" URL="src/core/trpc/procedures/docker-stats.procedure.ts" fillcolor="#ddfeff"] } } } } "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/trpc/trpc.ts" "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/database/repository.ts" diff --git a/dependency-graph.mmd b/dependency-graph.mmd index 0197483..b845e1e 100644 --- a/dependency-graph.mmd +++ b/dependency-graph.mmd @@ -149,6 +149,7 @@ Y-->Z 11-->A 11-->I 11-->12 +13-->A 13-->D 13-->O 13-->14 @@ -174,6 +175,7 @@ Y-->Z 1F-->1E 1F-->D 1F-->A +1F-->I 1G-->1E 1G-->D 1G-->O diff --git a/dependency-graph.svg b/dependency-graph.svg index 89342e8..412e558 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -4,82 +4,82 @@ - - + + dependency-cruiser output - + cluster_fs - -fs + +fs cluster_src - -src + +src cluster_src/core - -core + +core cluster_src/core/database - -database + +database cluster_src/core/docker - -docker + +docker cluster_src/core/plugins - -plugins + +plugins cluster_src/core/stacks - -stacks + +stacks cluster_src/core/trpc - -trpc + +trpc cluster_src/core/trpc/procedures - -procedures + +procedures cluster_src/core/utils - -utils + +utils cluster_src/middleware - -middleware + +middleware cluster_src/routes - -routes + +routes cluster_src/typings - -typings + +typings bun - -bun + +bun @@ -87,8 +87,8 @@ bun:sqlite - -bun:sqlite + +bun:sqlite @@ -96,8 +96,8 @@ events - -events + +events @@ -105,8 +105,8 @@ fs - -fs + +fs @@ -114,8 +114,8 @@ fs/promises - -promises + +promises @@ -123,8 +123,8 @@ package.json - -package.json + +package.json @@ -132,8 +132,8 @@ path - -path + +path @@ -141,8 +141,8 @@ src/core/database/helper.ts - -helper.ts + +helper.ts @@ -150,471 +150,477 @@ src/core/utils/logger.ts - -logger.ts + +logger.ts src/core/database/helper.ts->src/core/utils/logger.ts - - - - + + + + - + src/core/utils/logger.ts->path - - + + src/core/database/repository.ts - -repository.ts + +repository.ts - + src/core/utils/logger.ts->src/core/database/repository.ts - - - - + + + + src/routes/live-logs.ts - -live-logs.ts + +live-logs.ts - + src/core/utils/logger.ts->src/routes/live-logs.ts - - - - + + + + src/typings/websocket.ts - -websocket.ts + +websocket.ts - + src/core/utils/logger.ts->src/typings/websocket.ts - - + + src/core/database/repository.ts->bun:sqlite - - + + src/core/database/repository.ts->src/core/database/helper.ts - - - - + + + + src/core/database/repository.ts->src/core/utils/logger.ts - - - - + + + + src/typings/database.ts - -database.ts + +database.ts src/core/database/repository.ts->src/typings/database.ts - - + + src/typings/docker.ts - -docker.ts + +docker.ts src/core/database/repository.ts->src/typings/docker.ts - - + + src/core/docker/client.ts - -client.ts + +client.ts src/core/docker/client.ts->src/core/utils/logger.ts - - + + src/core/docker/client.ts->src/typings/docker.ts - - + + src/core/docker/monitor.ts - -monitor.ts + +monitor.ts src/core/docker/monitor.ts->bun - - + + src/core/docker/monitor.ts->src/core/utils/logger.ts - - + + src/core/docker/monitor.ts->src/core/database/repository.ts - - + + src/core/docker/monitor.ts->src/typings/docker.ts - - + + src/core/docker/monitor.ts->src/core/docker/client.ts - - + + src/core/plugins/plugin-manager.ts - -plugin-manager.ts + +plugin-manager.ts src/core/docker/monitor.ts->src/core/plugins/plugin-manager.ts - - + + - + src/core/plugins/plugin-manager.ts->events - - + + - + src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts - - + + - + src/core/plugins/plugin-manager.ts->src/typings/docker.ts - - + + src/typings/plugin.ts - -plugin.ts + +plugin.ts - + src/core/plugins/plugin-manager.ts->src/typings/plugin.ts - - + + src/core/docker/scheduler.ts - -scheduler.ts + +scheduler.ts src/core/docker/scheduler.ts->src/core/utils/logger.ts - - + + src/core/docker/scheduler.ts->src/core/database/repository.ts - - + + src/core/docker/scheduler.ts->src/typings/database.ts - - + + src/core/docker/store-host-stats.ts - -store-host-stats.ts + +store-host-stats.ts src/core/docker/scheduler.ts->src/core/docker/store-host-stats.ts - - + + src/core/docker/store-container-stats.ts - -store-container-stats.ts + +store-container-stats.ts src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/utils/logger.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/database/repository.ts - - + + - + src/core/docker/store-host-stats.ts->src/typings/docker.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/docker/client.ts - - + + src/typings/dockerode.ts - -dockerode.ts + +dockerode.ts - + src/core/docker/store-host-stats.ts->src/typings/dockerode.ts - - + + - + +src/core/docker/store-container-stats.ts->src/core/utils/logger.ts + + + + + src/core/docker/store-container-stats.ts->src/core/database/repository.ts - - + + - + src/core/docker/store-container-stats.ts->src/core/docker/client.ts - - + + src/core/utils/calculations.ts - -calculations.ts + +calculations.ts - + src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts - - + + src/core/plugins/loader.ts - -loader.ts + +loader.ts - + src/core/plugins/loader.ts->fs - - + + - + src/core/plugins/loader.ts->path - - + + - + src/core/plugins/loader.ts->src/core/utils/logger.ts - - + + - + src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts - - + + src/core/utils/change-me-checker.ts - -change-me-checker.ts + +change-me-checker.ts - + src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts - - + + - + src/core/utils/change-me-checker.ts->fs/promises - - + + - + src/core/utils/change-me-checker.ts->src/core/utils/logger.ts - - + + - + src/typings/plugin.ts->src/typings/docker.ts - - + + src/core/stacks/controller.ts - -controller.ts + +controller.ts - + src/core/stacks/controller.ts->src/core/utils/logger.ts - - + + - + src/core/stacks/controller.ts->src/core/database/repository.ts - - + + - + src/core/stacks/controller.ts->src/typings/database.ts - - + + src/typings/docker-compose.ts - -docker-compose.ts + +docker-compose.ts - + src/core/stacks/controller.ts->src/typings/docker-compose.ts - - + + src/core/trpc/index.ts - -index.ts + +index.ts @@ -622,267 +628,273 @@ src/core/trpc/router.ts - -router.ts + +router.ts - + src/core/trpc/index.ts->src/core/trpc/router.ts - - + + src/core/trpc/procedures/api-config.procedure.ts - -api-config.procedure.ts + +api-config.procedure.ts - + src/core/trpc/router.ts->src/core/trpc/procedures/api-config.procedure.ts - - + + src/core/trpc/trpc.ts - -trpc.ts + +trpc.ts - + src/core/trpc/router.ts->src/core/trpc/trpc.ts - - + + src/core/trpc/procedures/docker-manager.procedure.ts - -docker-manager.procedure.ts + +docker-manager.procedure.ts - + src/core/trpc/router.ts->src/core/trpc/procedures/docker-manager.procedure.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts - -docker-stats.procedure.ts + +docker-stats.procedure.ts - + src/core/trpc/router.ts->src/core/trpc/procedures/docker-stats.procedure.ts - - + + src/core/trpc/procedures/logs.procedure.ts - -logs.procedure.ts + +logs.procedure.ts - + src/core/trpc/router.ts->src/core/trpc/procedures/logs.procedure.ts - - + + src/core/trpc/procedures/stacks.procedure.ts - -stacks.procedure.ts + +stacks.procedure.ts - + src/core/trpc/router.ts->src/core/trpc/procedures/stacks.procedure.ts - - + + - + src/core/trpc/procedures/api-config.procedure.ts->src/core/utils/logger.ts - - + + - + src/core/trpc/procedures/api-config.procedure.ts->src/core/database/repository.ts - - + + - + src/core/trpc/procedures/api-config.procedure.ts->src/typings/database.ts - - + + - + src/core/trpc/procedures/api-config.procedure.ts->src/core/trpc/trpc.ts - - + + src/core/utils/package-json.ts - -package-json.ts + +package-json.ts - + src/core/trpc/procedures/api-config.procedure.ts->src/core/utils/package-json.ts - - + + - + src/core/utils/package-json.ts->package.json - - + + - + src/core/trpc/procedures/docker-manager.procedure.ts->src/core/utils/logger.ts - - + + - + src/core/trpc/procedures/docker-manager.procedure.ts->src/core/database/repository.ts - - + + + + + +src/core/trpc/procedures/docker-manager.procedure.ts->src/typings/docker.ts + + - + src/core/trpc/procedures/docker-manager.procedure.ts->src/core/trpc/trpc.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/utils/logger.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/database/repository.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/docker.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/docker/client.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/utils/calculations.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/dockerode.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/trpc/trpc.ts - - + + - + src/core/trpc/procedures/logs.procedure.ts->src/core/utils/logger.ts - - + + - + src/core/trpc/procedures/logs.procedure.ts->src/core/database/repository.ts - - + + - + src/core/trpc/procedures/logs.procedure.ts->src/core/trpc/trpc.ts - - + + - + src/core/trpc/procedures/stacks.procedure.ts->src/core/utils/logger.ts - - + + - + src/core/trpc/procedures/stacks.procedure.ts->src/core/database/repository.ts - - + + - + src/core/trpc/procedures/stacks.procedure.ts->src/core/stacks/controller.ts - - + + - + src/core/trpc/procedures/stacks.procedure.ts->src/core/trpc/trpc.ts - - + + - + src/routes/live-logs.ts->src/core/utils/logger.ts - - - - + + + + - + src/routes/live-logs.ts->src/typings/websocket.ts - - + + @@ -894,427 +906,427 @@ - + src/typings/websocket.ts->stream - - + + src/core/utils/respone-handler.ts - -respone-handler.ts + +respone-handler.ts - + src/core/utils/respone-handler.ts->src/core/utils/logger.ts - - + + src/typings/elysiajs.ts - -elysiajs.ts + +elysiajs.ts - + src/core/utils/respone-handler.ts->src/typings/elysiajs.ts - - + + src/index.ts - -index.ts + +index.ts - + src/index.ts->src/core/utils/logger.ts - - + + - + src/index.ts->src/core/database/repository.ts - - + + - + src/index.ts->src/typings/database.ts - - + + - + src/index.ts->src/core/docker/monitor.ts - - + + - + src/index.ts->src/core/docker/scheduler.ts - - + + - + src/index.ts->src/core/plugins/loader.ts - - + + - + src/index.ts->src/core/trpc/index.ts - - + + - + src/index.ts->src/routes/live-logs.ts - - + + src/middleware/auth.ts - -auth.ts + +auth.ts - + src/index.ts->src/middleware/auth.ts - - + + src/routes/stacks.ts - -stacks.ts + +stacks.ts - + src/index.ts->src/routes/stacks.ts - - + + src/routes/utils.ts - -utils.ts + +utils.ts - + src/index.ts->src/routes/utils.ts - - + + src/routes/api-config.ts - -api-config.ts + +api-config.ts - + src/index.ts->src/routes/api-config.ts - - + + src/routes/docker-manager.ts - -docker-manager.ts + +docker-manager.ts - + src/index.ts->src/routes/docker-manager.ts - - + + src/routes/docker-stats.ts - -docker-stats.ts + +docker-stats.ts - + src/index.ts->src/routes/docker-stats.ts - - + + src/routes/docker-websocket.ts - -docker-websocket.ts + +docker-websocket.ts - + src/index.ts->src/routes/docker-websocket.ts - - + + src/routes/logs.ts - -logs.ts + +logs.ts - + src/index.ts->src/routes/logs.ts - - + + - + src/middleware/auth.ts->src/core/utils/logger.ts - - + + - + src/middleware/auth.ts->src/core/database/repository.ts - - + + - + src/middleware/auth.ts->src/typings/database.ts - - + + - + src/middleware/auth.ts->src/typings/elysiajs.ts - - + + - + src/routes/stacks.ts->src/core/utils/logger.ts - - + + - + src/routes/stacks.ts->src/core/database/repository.ts - - + + - + src/routes/stacks.ts->src/core/stacks/controller.ts - - + + - + src/routes/stacks.ts->src/core/utils/respone-handler.ts - - + + - + src/routes/utils.ts->src/core/utils/package-json.ts - - + + - + src/routes/utils.ts->src/core/utils/respone-handler.ts - - + + - + src/routes/api-config.ts->src/core/utils/logger.ts - - + + - + src/routes/api-config.ts->src/core/database/repository.ts - - + + - + src/routes/api-config.ts->src/typings/database.ts - - + + - + src/routes/api-config.ts->src/core/plugins/plugin-manager.ts - - + + - + src/routes/api-config.ts->src/core/utils/package-json.ts - - + + - + src/routes/api-config.ts->src/core/utils/respone-handler.ts - - + + - + src/routes/api-config.ts->src/middleware/auth.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-manager.ts->src/core/database/repository.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/respone-handler.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-stats.ts->src/core/database/repository.ts - - + + - + src/routes/docker-stats.ts->src/typings/docker.ts - - + + - + src/routes/docker-stats.ts->src/core/docker/client.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-stats.ts->src/typings/dockerode.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/respone-handler.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-websocket.ts->src/core/database/repository.ts - - + + - + src/routes/docker-websocket.ts->src/core/docker/client.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/respone-handler.ts - - + + - + src/routes/docker-websocket.ts->stream - - + + - + src/routes/logs.ts->src/core/utils/logger.ts - - + + - + src/routes/logs.ts->src/core/database/repository.ts - - + + From 6131d8a45bb5d510e7b12b2bf12e6e6a03f9f3b9 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 21 Mar 2025 19:42:00 +0100 Subject: [PATCH 210/324] Fix: Add dependant docker socket prroxy --- .github/workflows/docker.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 90e231b..b3145bd 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -23,6 +23,10 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Start proxy + run: | + docker compose -f docker/docker-compose.dev.yaml up -d + - name: Run Unit-tests run: | bun install From c63e0ebd02ddf40d76a559a8614fc6f5ae32c0b2 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 21 Mar 2025 19:43:42 +0100 Subject: [PATCH 211/324] Fix: Name have to be written lowercase? --- .github/workflows/docker.yaml | 2 ++ docker/docker-compose.dev.yaml | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index b3145bd..e5d5779 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -25,6 +25,8 @@ jobs: - name: Start proxy run: | + pwd + ls -lah docker compose -f docker/docker-compose.dev.yaml up -d - name: Run Unit-tests diff --git a/docker/docker-compose.dev.yaml b/docker/docker-compose.dev.yaml index 39da6d6..5dc2c33 100644 --- a/docker/docker-compose.dev.yaml +++ b/docker/docker-compose.dev.yaml @@ -1,7 +1,7 @@ name: "DockStatAPI - Dev" services: socket-proxy: - container_name: Socket-Proxy + container_name: socket-proxy image: lscr.io/linuxserver/socket-proxy:latest volumes: - /var/run/docker.sock:/var/run/docker.sock:ro @@ -42,7 +42,7 @@ services: - VOLUMES=1 #optional sqlite-web: - container_name: SQLite-web + container_name: qlite-web image: ghcr.io/coleifer/sqlite-web:latest ports: - 8080:8080 From d3178ae6469a3e5388158a40709dcdcdd33270b5 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 21 Mar 2025 19:45:29 +0100 Subject: [PATCH 212/324] Fixx: That name? --- .dockerignore | 1 + docker/docker-compose.dev.yaml | 2 +- out | 18 ------------------ 3 files changed, 2 insertions(+), 19 deletions(-) delete mode 100644 out diff --git a/.dockerignore b/.dockerignore index 295585f..e6dbc5a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,6 +4,7 @@ *.md /docker *.dot +*.svg *.mmd *.lock src/tests diff --git a/docker/docker-compose.dev.yaml b/docker/docker-compose.dev.yaml index 5dc2c33..cca8f34 100644 --- a/docker/docker-compose.dev.yaml +++ b/docker/docker-compose.dev.yaml @@ -1,4 +1,4 @@ -name: "DockStatAPI - Dev" +name: "dockstatapi-dev" services: socket-proxy: container_name: socket-proxy diff --git a/out b/out deleted file mode 100644 index 8ca3fef..0000000 --- a/out +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - From b79ca8e59f654189ae4ab726bf4ac6e14e7087fc Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 21 Mar 2025 19:48:17 +0100 Subject: [PATCH 213/324] Fix: Adjustments to checking for container start --- .github/workflows/docker.yaml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index e5d5779..b917f7f 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -39,11 +39,16 @@ jobs: run: | bun build:docker - - name: Start docker container + - name: Start Docker container and check uptime run: | docker run --name dockstatapi --rm -d dockstatapi:local - sleep 10 - if [[ $(docker container ls | grep "Up" | wc -l) -gt 0 ]]; then docker kill dockstatapi && exit 0; else; exit 1; fi + sleep 30 + if docker ps --filter "name=dockstatapi" --filter "status=running" | grep dockstatapi; then + docker kill dockstatapi + exit 0 + else + exit 1 + fi release: name: Build and Push Docker Image on Release From 8579a13959e01f0ec3ca6fa25da1a79181a9e26e Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 21 Mar 2025 19:57:52 +0100 Subject: [PATCH 214/324] Fix: Docs update --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 87fea6f..fad0699 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Docker monitoring API with real-time statistics, stack management, and plugin su - **Database**: SQLite (WAL mode) - **Docker**: dockerode + compose - **Monitoring**: Custom metrics collection -- **Auth**: (TODO - Currently open) +- **Auth**: [Authentication](https://outline.itsnik.de/s/dockstat/doc/authentication-VSGhxqjtXf) ## Documentation and Wiki @@ -36,4 +36,4 @@ Please see [DockStatAPI](https://dockstatapi.itsnik.de) ![Dependency Graph](./dependency-graph.svg) -Click [here](./dependency-graph.mmd) for the mermaid version +Click [here](./dependency-graph.mmd) for the mermaid version. From 4828551a7c23d7c5ddfee39c757ebbf3c2b89b96 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 21 Mar 2025 20:11:56 +0100 Subject: [PATCH 215/324] Fixxx: Mismatch in error message for deleteDockerHost parameter. Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- src/core/database/repository.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/database/repository.ts b/src/core/database/repository.ts index 96ab533..34c3619 100644 --- a/src/core/database/repository.ts +++ b/src/core/database/repository.ts @@ -260,7 +260,7 @@ export const dbFunctions = { () => { if (typeof id !== "number") { logger.error("Invalid parameter type for deleteDockerHost"); - throw new TypeError("Name parameter must be a string"); + throw new TypeError("id parameter must be a number"); } } ); From 992095263ee6369b6a3de3c38d5d1e2ddba58192 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 21 Mar 2025 20:12:22 +0100 Subject: [PATCH 216/324] Fix: Falsy check for a boolean flag may lead to false positives. Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- src/routes/stacks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/stacks.ts b/src/routes/stacks.ts index c4e6c2c..ad96194 100644 --- a/src/routes/stacks.ts +++ b/src/routes/stacks.ts @@ -25,7 +25,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) if (!body.compose_spec) { missingParams.push("compose_spec"); } - if (!body.automatic_reboot_on_error) { + if (body.automatic_reboot_on_error === undefined) { missingParams.push("automatic_reboot_on_error"); } if (!body.source) { From 69faa814a2526eac7801a91d97b900155a40e9c9 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 21 Mar 2025 20:12:43 +0100 Subject: [PATCH 217/324] Fix: Unreachable code in logger format function. Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- src/core/utils/logger.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts index 903df1f..fce5909 100644 --- a/src/core/utils/logger.ts +++ b/src/core/utils/logger.ts @@ -132,10 +132,6 @@ export const logger = createLogger({ } return `${coloredLevel} [ ${coloredTimestamp} ] - ${formattedMessage} - [ ${coloredContext} ]`; - - const fullMessage = `${coloredLevel} [ ${coloredTimestamp} ] - ${message} - [ ${coloredContext} ]`; - - return formatTerminalMessage(fullMessage, prefixLength); }), ), transports: [new transports.Console()], From aaa8c73da01d1fea462b8060c9af574e8da8ce94 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 21 Mar 2025 20:14:03 +0100 Subject: [PATCH 218/324] Fix: Typo Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fad0699..3638459 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Docker monitoring API with real-time statistics, stack management, and plugin su ## Tech Stack -- **Runtime**: [Bun.sh](http://Bun.sh) +- **Runtime**: [Bun.sh](https://bun.sh) - **Framework**: [Elysia.js](https://elysiajs.com/) - **Database**: SQLite (WAL mode) - **Docker**: dockerode + compose From 8c9f59a30150a99a0d89c11ada95a1f83cc0e306 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 21 Mar 2025 20:16:33 +0100 Subject: [PATCH 219/324] Fix: See https://github.com/Its4Nik/DockStatAPI/pull/43#pullrequestreview-2706962510 --- src/core/database/repository.ts | 24 +++++----- src/core/docker/client.ts | 6 +-- .../procedures/docker-manager.procedure.ts | 8 ++-- src/core/utils/calculations.ts | 9 ++++ ...respone-handler.ts => response-handler.ts} | 0 src/middleware/auth.ts | 3 +- src/routes/api-config.ts | 20 ++++----- src/routes/docker-manager.ts | 6 +-- src/routes/docker-stats.ts | 2 +- src/routes/docker-websocket.ts | 8 ++-- src/routes/stacks.ts | 44 +++++++++---------- src/routes/utils.ts | 2 +- src/tests/post.spec.ts | 8 ++-- src/typings/docker.ts | 2 +- 14 files changed, 76 insertions(+), 66 deletions(-) rename src/core/utils/{respone-handler.ts => response-handler.ts} (100%) diff --git a/src/core/database/repository.ts b/src/core/database/repository.ts index 96ab533..680affe 100644 --- a/src/core/database/repository.ts +++ b/src/core/database/repository.ts @@ -33,7 +33,7 @@ export const dbFunctions = { CREATE TABLE IF NOT EXISTS docker_hosts ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, - hostadress TEXT NOT NULL, + hostAddress TEXT NOT NULL, secure BOOLEAN NOT NULL ); @@ -100,7 +100,7 @@ export const dbFunctions = { logger.debug("Initializing default docker host (Localhost)"); const stmt = db.prepare( ` - INSERT INTO docker_hosts (name, hostadress, secure) VALUES (?, ?, ?) + INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?) ` ); stmt.run("Localhost", "localhost:2375", false); @@ -115,10 +115,10 @@ export const dbFunctions = { "Add Docker Host", () => { const stmt = db.prepare(` - INSERT INTO docker_hosts (name, hostadress, secure) + INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?) `); - return stmt.run(host.name, host.hostadress, host.secure); + return stmt.run(host.name, host.hostAddress, host.secure); }, () => { if (host.name.length < 1) { @@ -126,15 +126,15 @@ export const dbFunctions = { throw new Error("Invalid data provided - Hostname needed"); } - if (host.hostadress.length < 1) { - logger.error("Hostadress needed"); - throw new Error("Invalid data provided - Hostadress needed"); + if (host.hostAddress.length < 1) { + logger.error("hostAddress needed"); + throw new Error("Invalid data provided - hostAddress needed"); } if ( typeof host.name !== "string" || typeof host.secure !== "boolean" || - typeof host.hostadress !== "string" + typeof host.hostAddress !== "string" ) { logger.error("Invalid parameter types for addDockerHost"); throw new TypeError("Invalid parameter types for addDockerHost"); @@ -148,7 +148,7 @@ export const dbFunctions = { "Get Docker Hosts", () => { const stmt = db.prepare(` - SELECT id, name, hostadress, secure + SELECT id, name, hostAddress, secure FROM docker_hosts ORDER BY id DESC `); @@ -223,11 +223,11 @@ export const dbFunctions = { () => { const stmt = db.prepare(` UPDATE docker_hosts - SET hostadress = ?, secure = ?, name = ? + SET hostAddress = ?, secure = ?, name = ? WHERE id = ? `); return stmt.run( - host.hostadress, + host.hostAddress, host.secure, host.name, String(host.id) @@ -236,7 +236,7 @@ export const dbFunctions = { () => { if ( typeof host.name !== "string" || - typeof host.hostadress !== "string" || + typeof host.hostAddress !== "string" || typeof host.secure !== "boolean" || typeof host.id !== "number" ) { diff --git a/src/core/docker/client.ts b/src/core/docker/client.ts index 324178b..7d8e6ea 100644 --- a/src/core/docker/client.ts +++ b/src/core/docker/client.ts @@ -4,9 +4,9 @@ import { logger } from "~/core/utils/logger"; export const getDockerClient = (host: DockerHost): Docker => { try { - const inputUrl = host.hostadress.includes("://") - ? host.hostadress - : `${host.secure ? "https" : "http"}://${host.hostadress}`; + const inputUrl = host.hostAddress.includes("://") + ? host.hostAddress + : `${host.secure ? "https" : "http"}://${host.hostAddress}`; const parsedUrl = new URL(inputUrl); const hostAddress = parsedUrl.hostname; let port = parsedUrl.port diff --git a/src/core/trpc/procedures/docker-manager.procedure.ts b/src/core/trpc/procedures/docker-manager.procedure.ts index 93b6a97..7621a5a 100644 --- a/src/core/trpc/procedures/docker-manager.procedure.ts +++ b/src/core/trpc/procedures/docker-manager.procedure.ts @@ -7,14 +7,15 @@ import { DockerHost } from "~/typings/docker"; const addHostInput = z.object({ name: z.string(), - hostadress: z.string(), + hostAddress: z.string(), secure: z.boolean(), }); const updateHostInput = z.object({ name: z.string(), - hostadress: z.string(), + hostAddress: z.string(), secure: z.boolean(), + id: z.number(), }); export const dockerManagerProcedure = router({ @@ -35,8 +36,7 @@ export const dockerManagerProcedure = router({ updateHost: publicProcedure.input(updateHostInput).mutation(({ input }) => { try { - (input as unknown as DockerHost).id = "0"; - dbFunctions.updateDockerHost(input as DockerHost); + dbFunctions.updateDockerHost(input); return { success: true, message: `Updated docker host (${name})` }; } catch (error) { logger.error("Error updating docker host", error); diff --git a/src/core/utils/calculations.ts b/src/core/utils/calculations.ts index 3d7a81d..9428bf9 100644 --- a/src/core/utils/calculations.ts +++ b/src/core/utils/calculations.ts @@ -10,6 +10,15 @@ const calculateCpuPercent = (stats: Docker.ContainerStats): number => { stats.precpu_stats.cpu_usage.total_usage; const systemDelta = stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage; + + if (cpuDelta <= 0) { + return 0; + } + + if (systemDelta <= 0) { + return 0; + } + return (cpuDelta / systemDelta) * 100; }; diff --git a/src/core/utils/respone-handler.ts b/src/core/utils/response-handler.ts similarity index 100% rename from src/core/utils/respone-handler.ts rename to src/core/utils/response-handler.ts diff --git a/src/middleware/auth.ts b/src/middleware/auth.ts index 5ec3f19..2672f88 100644 --- a/src/middleware/auth.ts +++ b/src/middleware/auth.ts @@ -72,8 +72,7 @@ export async function validateApiKey(request: Request, set: set) { return { error: "Invalid API key" }; } - logger.info(`Valid API key used: ${apiKey}`); - return { apiKey }; + return logger.info(`Valid API key used`); } catch (error) { logger.error("Error during API key validation", error); set.status = 500; diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index a74f34b..17ea352 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -1,7 +1,7 @@ import { Elysia, t } from "elysia"; import { dbFunctions } from "~/core/database/repository"; import { logger } from "~/core/utils/logger"; -import { responseHandler } from "~/core/utils/respone-handler"; +import { responseHandler } from "~/core/utils/response-handler"; import { config } from "~/typings/database"; import { version, @@ -32,7 +32,7 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) return responseHandler.error( set, error as string, - "Error getting the DockStatAPI config", + "Error getting the DockStatAPI config" ); } }, @@ -41,7 +41,7 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) tags: ["Management"], description: "Returns DockStatAPI's config", }, - }, + } ) .get( "/plugins", @@ -52,11 +52,11 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) return responseHandler.error( set, error as string, - "Error getting all registered plugins", + "Error getting all registered plugins" ); } }, - { detail: { tags: ["Management"], description: "List all Plugin Names" } }, + { detail: { tags: ["Management"], description: "List all Plugin Names" } } ) .post( "/update", @@ -67,14 +67,14 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) dbFunctions.updateConfig( fetching_interval, keep_data_for, - await hashApiKey(api_key), + await hashApiKey(api_key) ); return responseHandler.ok(set, "Updated DockStatAPI config"); } catch (error) { return responseHandler.error( set, "Error updating the DockStatAPI config", - error as string, + error as string ); } }, @@ -88,7 +88,7 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) tags: ["Management"], description: "Update the current DockStatAPI config", }, - }, + } ) .get( "/package", @@ -110,7 +110,7 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) return responseHandler.error( set, error as string, - "Error while reading package.json", + "Error while reading package.json" ); } }, @@ -119,5 +119,5 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) tags: ["Management"], description: "Returns relevant information about the package.json", }, - }, + } ); diff --git a/src/routes/docker-manager.ts b/src/routes/docker-manager.ts index 6db7362..dcf97a7 100644 --- a/src/routes/docker-manager.ts +++ b/src/routes/docker-manager.ts @@ -1,7 +1,7 @@ import { Elysia, t } from "elysia"; import { dbFunctions } from "~/core/database/repository"; import { logger } from "~/core/utils/logger"; -import { responseHandler } from "~/core/utils/respone-handler"; +import { responseHandler } from "~/core/utils/response-handler"; export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) .post( @@ -26,7 +26,7 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) }, body: t.Object({ name: t.String(), - hostadress: t.String(), + hostAddress: t.String(), secure: t.Boolean(), }), } @@ -54,7 +54,7 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) body: t.Object({ id: t.Number(), name: t.String(), - hostadress: t.String(), + hostAddress: t.String(), secure: t.Boolean(), }), } diff --git a/src/routes/docker-stats.ts b/src/routes/docker-stats.ts index 4324fef..c86f688 100644 --- a/src/routes/docker-stats.ts +++ b/src/routes/docker-stats.ts @@ -7,7 +7,7 @@ import { calculateMemoryUsage, } from "~/core/utils/calculations"; import { logger } from "~/core/utils/logger"; -import { responseHandler } from "~/core/utils/respone-handler"; +import { responseHandler } from "~/core/utils/response-handler"; import type { ContainerInfo, DockerHost, HostStats } from "~/typings/docker"; import type { DockerInfo } from "~/typings/dockerode"; diff --git a/src/routes/docker-websocket.ts b/src/routes/docker-websocket.ts index bd9571b..c4444b4 100644 --- a/src/routes/docker-websocket.ts +++ b/src/routes/docker-websocket.ts @@ -7,7 +7,7 @@ import { calculateMemoryUsage, } from "~/core/utils/calculations"; import { logger } from "~/core/utils/logger"; -import { responseHandler } from "~/core/utils/respone-handler"; +import { responseHandler } from "~/core/utils/response-handler"; import split2 from "split2"; import type { Readable } from "stream"; @@ -60,13 +60,15 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( .on("close", () => splitStream.destroy()) .pipe(splitStream) .on("data", (line: string) => { - if (ws.readyState !== 1 || !line) return; + if (ws.readyState !== 1 || !line) { + return; + } try { const stats = JSON.parse(line); ws.send( JSON.stringify({ id: containerInfo.Id, - hostId: host.id as string, + hostId: host.id, name: containerInfo.Names[0].replace(/^\//, ""), image: containerInfo.Image, status: containerInfo.Status, diff --git a/src/routes/stacks.ts b/src/routes/stacks.ts index c4e6c2c..7c594d1 100644 --- a/src/routes/stacks.ts +++ b/src/routes/stacks.ts @@ -1,5 +1,5 @@ import { Elysia, t } from "elysia"; -import { responseHandler } from "~/core/utils/respone-handler"; +import { responseHandler } from "~/core/utils/response-handler"; import { deployStack, stopStack, @@ -48,18 +48,18 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) body.automatic_reboot_on_error, isCustom, image_updates, - body.stack_prefix, + body.stack_prefix ); logger.info(`Deployed Stack (${body.name})`); return responseHandler.ok( set, - `Stack ${body.name} deployed successfully`, + `Stack ${body.name} deployed successfully` ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error deploying stack", + "Error deploying stack" ); } }, @@ -79,7 +79,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) source: t.String(), stack_prefix: t.Optional(t.String()), }), - }, + } ) .post( "/start", @@ -92,13 +92,13 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) logger.info(`Started Stack (${body.stack})`); return responseHandler.ok( set, - `Stack ${body.stack} started successfully`, + `Stack ${body.stack} started successfully` ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error starting stack", + "Error starting stack" ); } }, @@ -107,7 +107,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) body: t.Object({ stack: t.Any(), }), - }, + } ) .post( "/stop", @@ -120,13 +120,13 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) logger.info(`Stopped Stack (${body.stack})`); return responseHandler.ok( set, - `Stack ${body.stack} stopped successfully`, + `Stack ${body.stack} stopped successfully` ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error stopping stack", + "Error stopping stack" ); } }, @@ -135,7 +135,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) body: t.Object({ stack: t.Any(), }), - }, + } ) .post( "/restart", @@ -148,13 +148,13 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) logger.info(`Restarted Stack (${body.stack})`); return responseHandler.ok( set, - `Stack ${body.stack} restarted successfully`, + `Stack ${body.stack} restarted successfully` ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error restarting stack", + "Error restarting stack" ); } }, @@ -163,7 +163,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) body: t.Object({ stack: t.Any(), }), - }, + } ) .post( "/pull-images", @@ -176,13 +176,13 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) logger.info(`Pulled Stack images (${body.stack})`); return responseHandler.ok( set, - `Images for stack ${body.stack} pulled successfully`, + `Images for stack ${body.stack} pulled successfully` ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error pulling images", + "Error pulling images" ); } }, @@ -194,7 +194,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) body: t.Object({ stack: t.Any(), }), - }, + } ) .get( "/status", @@ -206,7 +206,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) status = await getStackStatus(query.stack_name); res = responseHandler.ok( set, - `Stack ${query.stack_name} status retrieved successfully`, + `Stack ${query.stack_name} status retrieved successfully` ); logger.info("Fetched Stack status"); } else { @@ -219,7 +219,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) return responseHandler.error( set, error.message || error, - "Error getting stack status", + "Error getting stack status" ); } }, @@ -232,7 +232,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) query: t.Object({ stack_name: t.Any(), }), - }, + } ) .get( "/", @@ -245,7 +245,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) return responseHandler.error( set, error.message || error, - "Error getting stacks", + "Error getting stacks" ); } }, @@ -254,5 +254,5 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) tags: ["Stacks"], description: "Returns an Array of Stack-config-objects", }, - }, + } ); diff --git a/src/routes/utils.ts b/src/routes/utils.ts index 2253424..cb8b942 100644 --- a/src/routes/utils.ts +++ b/src/routes/utils.ts @@ -10,7 +10,7 @@ import { devDependencies, license, } from "~/core/utils/package-json"; -import { responseHandler } from "~/core/utils/respone-handler"; +import { responseHandler } from "~/core/utils/response-handler"; export const utilRoutes = new Elysia({ prefix: "/utils" }).get( "/info", diff --git a/src/tests/post.spec.ts b/src/tests/post.spec.ts index f581770..040012e 100644 --- a/src/tests/post.spec.ts +++ b/src/tests/post.spec.ts @@ -6,7 +6,7 @@ describe("DockStatAPI (POST)", () => { it("Check Host adding", async () => { const body = { name: "test", - hostadress: "localhost:2375", + hostAddress: "localhost:2375", secure: false, }; @@ -18,18 +18,18 @@ describe("DockStatAPI (POST)", () => { const codeBody: DockerHost = { id: 2, name: "test", - hostadress: "127.0.0.1:2375", + hostAddress: "127.0.0.1:2375", secure: false, }; await runTestCode("/docker-config/update-host", 200, "POST", codeBody); const responseBody: DockerHost[] = [ - { id: 2, name: "test", hostadress: "127.0.0.1:2375", secure: 0 }, + { id: 2, name: "test", hostAddress: "127.0.0.1:2375", secure: 0 }, { id: 1, name: "Localhost", - hostadress: "localhost:2375", + hostAddress: "localhost:2375", secure: 0, }, ]; diff --git a/src/typings/docker.ts b/src/typings/docker.ts index f295ab3..e6701f6 100644 --- a/src/typings/docker.ts +++ b/src/typings/docker.ts @@ -1,6 +1,6 @@ interface DockerHost { name: string; - hostadress: string; + hostAddress: string; secure: boolean | number; id?: number; } From df7b2b659aa2ec255dc980c5f88d58f1576015e4 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Fri, 21 Mar 2025 19:18:33 +0000 Subject: [PATCH 220/324] Update dependency graphs --- dependency-graph.dot | 18 +++++----- dependency-graph.mmd | 2 +- dependency-graph.svg | 86 ++++++++++++++++++++++---------------------- 3 files changed, 53 insertions(+), 53 deletions(-) diff --git a/dependency-graph.dot b/dependency-graph.dot index 48a4a93..21ecb01 100644 --- a/dependency-graph.dot +++ b/dependency-graph.dot @@ -111,9 +111,9 @@ strict digraph "dependency-cruiser output"{ "src/core/utils/logger.ts" -> "path" [style="dashed" penwidth="1.0"] subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/package-json.ts" [label= tooltip="package-json.ts" URL="src/core/utils/package-json.ts" fillcolor="#ddfeff"] } } } "src/core/utils/package-json.ts" -> "package.json" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/respone-handler.ts" [label= tooltip="respone-handler.ts" URL="src/core/utils/respone-handler.ts" fillcolor="#ddfeff"] } } } - "src/core/utils/respone-handler.ts" -> "src/core/utils/logger.ts" - "src/core/utils/respone-handler.ts" -> "src/typings/elysiajs.ts" [arrowhead="onormal" penwidth="1.0"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/response-handler.ts" [label= tooltip="response-handler.ts" URL="src/core/utils/response-handler.ts" fillcolor="#ddfeff"] } } } + "src/core/utils/response-handler.ts" -> "src/core/utils/logger.ts" + "src/core/utils/response-handler.ts" -> "src/typings/elysiajs.ts" [arrowhead="onormal" penwidth="1.0"] subgraph "cluster_src" {label="src" "src/index.ts" [label= tooltip="index.ts" URL="src/index.ts" fillcolor="#ddfeff"] } "src/index.ts" -> "src/core/docker/monitor.ts" "src/index.ts" -> "src/middleware/auth.ts" @@ -141,19 +141,19 @@ strict digraph "dependency-cruiser output"{ "src/routes/api-config.ts" -> "src/core/plugins/plugin-manager.ts" "src/routes/api-config.ts" -> "src/core/utils/logger.ts" "src/routes/api-config.ts" -> "src/core/utils/package-json.ts" - "src/routes/api-config.ts" -> "src/core/utils/respone-handler.ts" + "src/routes/api-config.ts" -> "src/core/utils/response-handler.ts" "src/routes/api-config.ts" -> "src/middleware/auth.ts" "src/routes/api-config.ts" -> "src/typings/database.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/docker-manager.ts" [label= tooltip="docker-manager.ts" URL="src/routes/docker-manager.ts" fillcolor="#ddfeff"] } } "src/routes/docker-manager.ts" -> "src/core/database/repository.ts" "src/routes/docker-manager.ts" -> "src/core/utils/logger.ts" - "src/routes/docker-manager.ts" -> "src/core/utils/respone-handler.ts" + "src/routes/docker-manager.ts" -> "src/core/utils/response-handler.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/docker-stats.ts" [label= tooltip="docker-stats.ts" URL="src/routes/docker-stats.ts" fillcolor="#ddfeff"] } } "src/routes/docker-stats.ts" -> "src/core/database/repository.ts" "src/routes/docker-stats.ts" -> "src/core/docker/client.ts" "src/routes/docker-stats.ts" -> "src/core/utils/calculations.ts" "src/routes/docker-stats.ts" -> "src/core/utils/logger.ts" - "src/routes/docker-stats.ts" -> "src/core/utils/respone-handler.ts" + "src/routes/docker-stats.ts" -> "src/core/utils/response-handler.ts" "src/routes/docker-stats.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] "src/routes/docker-stats.ts" -> "src/typings/dockerode.ts" [arrowhead="onormal" penwidth="1.0"] subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/docker-websocket.ts" [label= tooltip="docker-websocket.ts" URL="src/routes/docker-websocket.ts" fillcolor="#ddfeff"] } } @@ -161,7 +161,7 @@ strict digraph "dependency-cruiser output"{ "src/routes/docker-websocket.ts" -> "src/core/docker/client.ts" "src/routes/docker-websocket.ts" -> "src/core/utils/calculations.ts" "src/routes/docker-websocket.ts" -> "src/core/utils/logger.ts" - "src/routes/docker-websocket.ts" -> "src/core/utils/respone-handler.ts" + "src/routes/docker-websocket.ts" -> "src/core/utils/response-handler.ts" "src/routes/docker-websocket.ts" -> "stream" [style="dashed" penwidth="1.0" arrowhead="onormal"] subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/live-logs.ts" [label= tooltip="live-logs.ts" URL="src/routes/live-logs.ts" fillcolor="#ddfeff"] } } "src/routes/live-logs.ts" -> "src/core/utils/logger.ts" [arrowhead="normalnoneodot"] @@ -173,10 +173,10 @@ strict digraph "dependency-cruiser output"{ "src/routes/stacks.ts" -> "src/core/database/repository.ts" "src/routes/stacks.ts" -> "src/core/stacks/controller.ts" "src/routes/stacks.ts" -> "src/core/utils/logger.ts" - "src/routes/stacks.ts" -> "src/core/utils/respone-handler.ts" + "src/routes/stacks.ts" -> "src/core/utils/response-handler.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/utils.ts" [label= tooltip="utils.ts" URL="src/routes/utils.ts" fillcolor="#ddfeff"] } } "src/routes/utils.ts" -> "src/core/utils/package-json.ts" - "src/routes/utils.ts" -> "src/core/utils/respone-handler.ts" + "src/routes/utils.ts" -> "src/core/utils/response-handler.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/database.ts" [label= tooltip="database.ts" URL="src/typings/database.ts" fillcolor="#ddfeff"] } } subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/docker-compose.ts" [label= tooltip="docker-compose.ts" URL="src/typings/docker-compose.ts" fillcolor="#ddfeff"] } } subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/docker.ts" [label= tooltip="docker.ts" URL="src/typings/docker.ts" fillcolor="#ddfeff"] } } diff --git a/dependency-graph.mmd b/dependency-graph.mmd index b845e1e..da1b20e 100644 --- a/dependency-graph.mmd +++ b/dependency-graph.mmd @@ -22,7 +22,7 @@ subgraph 6["plugins"] end subgraph 9["utils"] A["logger.ts"] -W["respone-handler.ts"] +W["response-handler.ts"] Y["package-json.ts"] 14["calculations.ts"] 17["change-me-checker.ts"] diff --git a/dependency-graph.svg b/dependency-graph.svg index 412e558..b5a3b80 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -215,8 +215,8 @@ src/core/utils/logger.ts->src/typings/websocket.ts - - + + @@ -558,8 +558,8 @@ src/core/utils/change-me-checker.ts->fs/promises - - + + @@ -911,20 +911,20 @@ - + -src/core/utils/respone-handler.ts - - -respone-handler.ts +src/core/utils/response-handler.ts + + +response-handler.ts - + -src/core/utils/respone-handler.ts->src/core/utils/logger.ts - - +src/core/utils/response-handler.ts->src/core/utils/logger.ts + + @@ -935,11 +935,11 @@ - + -src/core/utils/respone-handler.ts->src/typings/elysiajs.ts - - +src/core/utils/response-handler.ts->src/typings/elysiajs.ts + + @@ -1121,8 +1121,8 @@ src/middleware/auth.ts->src/core/utils/logger.ts - - + + @@ -1139,8 +1139,8 @@ src/middleware/auth.ts->src/typings/elysiajs.ts - - + + @@ -1160,11 +1160,11 @@ - + -src/routes/stacks.ts->src/core/utils/respone-handler.ts - - +src/routes/stacks.ts->src/core/utils/response-handler.ts + + @@ -1172,11 +1172,11 @@ - + -src/routes/utils.ts->src/core/utils/respone-handler.ts - - +src/routes/utils.ts->src/core/utils/response-handler.ts + + @@ -1208,11 +1208,11 @@ - + -src/routes/api-config.ts->src/core/utils/respone-handler.ts - - +src/routes/api-config.ts->src/core/utils/response-handler.ts + + @@ -1232,9 +1232,9 @@ - + -src/routes/docker-manager.ts->src/core/utils/respone-handler.ts +src/routes/docker-manager.ts->src/core/utils/response-handler.ts @@ -1274,11 +1274,11 @@ - + -src/routes/docker-stats.ts->src/core/utils/respone-handler.ts - - +src/routes/docker-stats.ts->src/core/utils/response-handler.ts + + @@ -1304,11 +1304,11 @@ - + -src/routes/docker-websocket.ts->src/core/utils/respone-handler.ts - - +src/routes/docker-websocket.ts->src/core/utils/response-handler.ts + + From 690ab3c5f7bd92daa9359c51c0ffa96b66b813ee Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 21 Mar 2025 20:28:43 +0100 Subject: [PATCH 221/324] Fix: Knip adjustment --- .knip.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.knip.json b/.knip.json index 64b44a6..0c1cd54 100644 --- a/.knip.json +++ b/.knip.json @@ -1,5 +1,5 @@ { "entry": ["src/index.ts"], "project": ["src/**/*.ts"], - "ignore": ["src/plugins/*.plugin.ts"] + "ignore": ["src/plugins/*.plugin.ts","src/tests/*.ts"] } From 6118ad90c764d1078ed147e6e014ecd9560b3dcc Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 26 Mar 2025 16:37:41 +0100 Subject: [PATCH 222/324] Fix: Minor code adjustments --- .github/workflows/{docker.yaml => pipeline.yaml} | 4 +--- docker/Dockerfile | 2 +- package.json | 2 +- src/typings/websocket.ts | 14 +++++++------- 4 files changed, 10 insertions(+), 12 deletions(-) rename .github/workflows/{docker.yaml => pipeline.yaml} (97%) diff --git a/.github/workflows/docker.yaml b/.github/workflows/pipeline.yaml similarity index 97% rename from .github/workflows/docker.yaml rename to .github/workflows/pipeline.yaml index b917f7f..c4ddc1f 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/pipeline.yaml @@ -25,15 +25,13 @@ jobs: - name: Start proxy run: | - pwd - ls -lah docker compose -f docker/docker-compose.dev.yaml up -d - name: Run Unit-tests run: | bun install bun clean - bun test + bun test --reporter=junit --reporter-outfile=./bun.xml - name: Run Docker Build run: | diff --git a/docker/Dockerfile b/docker/Dockerfile index da82057..bc09e1b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -23,7 +23,7 @@ LABEL org.opencontainers.image.title="DockStatAPI" \ RUN apk add --no-cache curl -HEALTHCHECK --timeout=30s --start-period=5s --retries=3 \ +HEALTHCHECK --timeout=10s --start-period=2s --retries=3 \ CMD curl --fail http://localhost:3000/health || exit 1 VOLUME [ "/DockStatAPI/src/plugins" ] diff --git a/package.json b/package.json index 3a1dd22..b024205 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "version": "3.0.0", "scripts": { "start": "cross-env NODE_ENV=production LOG_LEVEL=info bun run src/index.ts", - "start:docker": "bun run build:docker && docker run -p 3001:3000 --rm -d --name dockstatapi -v 'plugins:/DockStatAPI/src/plugins' dockstatapi:local", + "start:docker": "bun run build:docker && docker run -p 3000:3000 --rm -d --name dockstatapi -v 'plugins:/DockStatAPI/src/plugins' dockstatapi:local", "dev": "docker compose -f docker/docker-compose.dev.yaml up -d && cross-env NODE_ENV=dev bun run --watch src/index.ts", "dev:clean": "bun dev ; echo '\nExiting...' ; bun clean", "build": "bun build --target bun src/index.ts --outdir ./dist", diff --git a/src/typings/websocket.ts b/src/typings/websocket.ts index 59f5e3a..1cb3821 100644 --- a/src/typings/websocket.ts +++ b/src/typings/websocket.ts @@ -1,10 +1,10 @@ -import type { Readable, Transform } from "stream"; -import type internal from "stream"; +//import type { Readable, Transform } from "stream"; +//import type internal from "stream"; -interface streams { - statsStream: Readable; - splitStream: internal.Transform; -} +//interface streams { +// statsStream: Readable; +// splitStream: internal.Transform; +//} interface logStreamData { timestamp: string; @@ -14,4 +14,4 @@ interface logStreamData { line: number; } -export { streams, logStreamData }; +export { logStreamData }; From 9b9945c597aa6a7e1417816b674d032bf21b1fc4 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 26 Mar 2025 15:38:15 +0000 Subject: [PATCH 223/324] Update dependency graphs --- dependency-graph.dot | 1 - dependency-graph.mmd | 243 +++++++------ dependency-graph.svg | 832 +++++++++++++++++++++---------------------- 3 files changed, 534 insertions(+), 542 deletions(-) diff --git a/dependency-graph.dot b/dependency-graph.dot index 21ecb01..254698f 100644 --- a/dependency-graph.dot +++ b/dependency-graph.dot @@ -185,6 +185,5 @@ strict digraph "dependency-cruiser output"{ subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/plugin.ts" [label= tooltip="plugin.ts" URL="src/typings/plugin.ts" fillcolor="#ddfeff"] } } "src/typings/plugin.ts" -> "src/typings/docker.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/websocket.ts" [label= tooltip="websocket.ts" URL="src/typings/websocket.ts" fillcolor="#ddfeff"] } } - "src/typings/websocket.ts" -> "stream" [style="dashed" penwidth="1.0" arrowhead="onormal"] "stream" [label= tooltip="stream" URL="https://nodejs.org/api/stream.html" color="grey" fontcolor="grey"] } diff --git a/dependency-graph.mmd b/dependency-graph.mmd index da1b20e..ad13d50 100644 --- a/dependency-graph.mmd +++ b/dependency-graph.mmd @@ -11,100 +11,100 @@ subgraph 0["src"] subgraph 2["core"] subgraph 3["docker"] 4["monitor.ts"] -O["client.ts"] -10["scheduler.ts"] -11["store-host-stats.ts"] -13["store-container-stats.ts"] +N["client.ts"] +Z["scheduler.ts"] +10["store-host-stats.ts"] +12["store-container-stats.ts"] end subgraph 6["plugins"] 7["plugin-manager.ts"] -15["loader.ts"] +14["loader.ts"] end subgraph 9["utils"] A["logger.ts"] -W["response-handler.ts"] -Y["package-json.ts"] -14["calculations.ts"] -17["change-me-checker.ts"] +V["response-handler.ts"] +X["package-json.ts"] +13["calculations.ts"] +16["change-me-checker.ts"] end subgraph C["database"] D["repository.ts"] F["helper.ts"] end -subgraph T["stacks"] -U["controller.ts"] +subgraph S["stacks"] +T["controller.ts"] end -subgraph 19["trpc"] -1A["index.ts"] -1B["router.ts"] -subgraph 1C["procedures"] -1D["api-config.procedure.ts"] -1F["docker-manager.procedure.ts"] -1G["docker-stats.procedure.ts"] -1H["logs.procedure.ts"] -1I["stacks.procedure.ts"] +subgraph 18["trpc"] +19["index.ts"] +1A["router.ts"] +subgraph 1B["procedures"] +1C["api-config.procedure.ts"] +1E["docker-manager.procedure.ts"] +1F["docker-stats.procedure.ts"] +1G["logs.procedure.ts"] +1H["stacks.procedure.ts"] end -1E["trpc.ts"] +1D["trpc.ts"] end end subgraph G["typings"] H["database.ts"] I["docker.ts"] L["websocket.ts"] -N["plugin.ts"] -R["elysiajs.ts"] -V["docker-compose.ts"] -12["dockerode.ts"] +M["plugin.ts"] +Q["elysiajs.ts"] +U["docker-compose.ts"] +11["dockerode.ts"] end subgraph J["routes"] K["live-logs.ts"] -S["stacks.ts"] -X["utils.ts"] -1J["api-config.ts"] -1K["docker-manager.ts"] -1L["docker-stats.ts"] -1M["docker-websocket.ts"] +R["stacks.ts"] +W["utils.ts"] +1I["api-config.ts"] +1J["docker-manager.ts"] +1K["docker-stats.ts"] +1L["docker-websocket.ts"] 1N["logs.ts"] end -subgraph P["middleware"] -Q["auth.ts"] +subgraph O["middleware"] +P["auth.ts"] end end 5["bun"] 8["events"] B["path"] E["bun:sqlite"] -M["stream"] -Z["package.json"] -subgraph 16["fs"] -18["promises"] +Y["package.json"] +subgraph 15["fs"] +17["promises"] end +1M["stream"] 1-->4 -1-->Q +1-->P 1-->K -1-->S -1-->X +1-->R +1-->W 1-->H 1-->D -1-->10 -1-->15 -1-->1A +1-->Z +1-->14 +1-->19 1-->A +1-->1I 1-->1J 1-->1K 1-->1L -1-->1M 1-->1N 4-->7 4-->D -4-->O +4-->N 4-->A 4-->I 4-->I 4-->5 7-->A 7-->I -7-->N +7-->M 7-->8 A-->D A-->K @@ -118,101 +118,100 @@ D-->E F-->A K-->A K-->L -L-->M +M-->I +N-->A N-->I -O-->A -O-->I -Q-->D -Q-->A -Q-->H -Q-->R -S-->D -S-->U -S-->A -S-->W -U-->D -U-->A -U-->H -U-->V -W-->A -W-->R +P-->D +P-->A +P-->H +P-->Q +R-->D +R-->T +R-->A +R-->V +T-->D +T-->A +T-->H +T-->U +V-->A +V-->Q +W-->X +W-->V X-->Y -X-->W -Y-->Z +Z-->D +Z-->10 +Z-->12 +Z-->A +Z-->H 10-->D -10-->11 -10-->13 +10-->N 10-->A -10-->H -11-->D -11-->O -11-->A -11-->I -11-->12 -13-->A -13-->D -13-->O -13-->14 -15-->17 -15-->A -15-->7 -15-->16 -15-->B -17-->A -17-->18 -1A-->1B -1B-->1D -1B-->1F -1B-->1G -1B-->1H -1B-->1I -1B-->1E -1D-->1E -1D-->D -1D-->A -1D-->Y -1D-->H -1F-->1E +10-->I +10-->11 +12-->A +12-->D +12-->N +12-->13 +14-->16 +14-->A +14-->7 +14-->15 +14-->B +16-->A +16-->17 +19-->1A +1A-->1C +1A-->1E +1A-->1F +1A-->1G +1A-->1H +1A-->1D +1C-->1D +1C-->D +1C-->A +1C-->X +1C-->H +1E-->1D +1E-->D +1E-->A +1E-->I +1F-->1D 1F-->D +1F-->N +1F-->13 1F-->A 1F-->I -1G-->1E +1F-->11 +1G-->1D 1G-->D -1G-->O -1G-->14 1G-->A -1G-->I -1G-->12 -1H-->1E +1H-->1D 1H-->D +1H-->T 1H-->A -1I-->1E 1I-->D -1I-->U +1I-->7 1I-->A +1I-->X +1I-->V +1I-->P +1I-->H 1J-->D -1J-->7 1J-->A -1J-->Y -1J-->W -1J-->Q -1J-->H +1J-->V 1K-->D +1K-->N +1K-->13 1K-->A -1K-->W +1K-->V +1K-->I +1K-->11 1L-->D -1L-->O -1L-->14 +1L-->N +1L-->13 1L-->A -1L-->W -1L-->I -1L-->12 -1M-->D -1M-->O -1M-->14 -1M-->A -1M-->W -1M-->M +1L-->V +1L-->1M 1N-->D 1N-->A diff --git a/dependency-graph.svg b/dependency-graph.svg index b5a3b80..715f66a 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -4,82 +4,82 @@ - - + + dependency-cruiser output - + cluster_fs - -fs + +fs cluster_src - -src + +src cluster_src/core - -core + +core cluster_src/core/database - -database + +database cluster_src/core/docker - -docker + +docker cluster_src/core/plugins - -plugins + +plugins cluster_src/core/stacks - -stacks + +stacks cluster_src/core/trpc - -trpc + +trpc cluster_src/core/trpc/procedures - -procedures + +procedures cluster_src/core/utils - -utils + +utils cluster_src/middleware - -middleware + +middleware cluster_src/routes - -routes + +routes cluster_src/typings - -typings + +typings bun - -bun + +bun @@ -87,8 +87,8 @@ bun:sqlite - -bun:sqlite + +bun:sqlite @@ -96,8 +96,8 @@ events - -events + +events @@ -105,8 +105,8 @@ fs - -fs + +fs @@ -114,8 +114,8 @@ fs/promises - -promises + +promises @@ -123,8 +123,8 @@ package.json - -package.json + +package.json @@ -132,8 +132,8 @@ path - -path + +path @@ -141,8 +141,8 @@ src/core/database/helper.ts - -helper.ts + +helper.ts @@ -150,477 +150,477 @@ src/core/utils/logger.ts - -logger.ts + +logger.ts src/core/database/helper.ts->src/core/utils/logger.ts - - - - + + + + src/core/utils/logger.ts->path - - + + src/core/database/repository.ts - -repository.ts + +repository.ts src/core/utils/logger.ts->src/core/database/repository.ts - - - - + + + + src/routes/live-logs.ts - -live-logs.ts + +live-logs.ts src/core/utils/logger.ts->src/routes/live-logs.ts - - - - + + + + src/typings/websocket.ts - -websocket.ts + +websocket.ts src/core/utils/logger.ts->src/typings/websocket.ts - - + + src/core/database/repository.ts->bun:sqlite - - + + src/core/database/repository.ts->src/core/database/helper.ts - - - - + + + + src/core/database/repository.ts->src/core/utils/logger.ts - - - - + + + + src/typings/database.ts - -database.ts + +database.ts src/core/database/repository.ts->src/typings/database.ts - - + + src/typings/docker.ts - -docker.ts + +docker.ts src/core/database/repository.ts->src/typings/docker.ts - - + + src/core/docker/client.ts - -client.ts + +client.ts src/core/docker/client.ts->src/core/utils/logger.ts - - + + src/core/docker/client.ts->src/typings/docker.ts - - + + src/core/docker/monitor.ts - -monitor.ts + +monitor.ts src/core/docker/monitor.ts->bun - - + + src/core/docker/monitor.ts->src/core/utils/logger.ts - - + + src/core/docker/monitor.ts->src/core/database/repository.ts - - + + src/core/docker/monitor.ts->src/typings/docker.ts - - + + src/core/docker/monitor.ts->src/core/docker/client.ts - - + + src/core/plugins/plugin-manager.ts - -plugin-manager.ts + +plugin-manager.ts src/core/docker/monitor.ts->src/core/plugins/plugin-manager.ts - - + + src/core/plugins/plugin-manager.ts->events - - + + src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts - - + + src/core/plugins/plugin-manager.ts->src/typings/docker.ts - - + + src/typings/plugin.ts - -plugin.ts + +plugin.ts src/core/plugins/plugin-manager.ts->src/typings/plugin.ts - - + + src/core/docker/scheduler.ts - -scheduler.ts + +scheduler.ts src/core/docker/scheduler.ts->src/core/utils/logger.ts - - + + src/core/docker/scheduler.ts->src/core/database/repository.ts - - + + src/core/docker/scheduler.ts->src/typings/database.ts - - + + src/core/docker/store-host-stats.ts - -store-host-stats.ts + +store-host-stats.ts src/core/docker/scheduler.ts->src/core/docker/store-host-stats.ts - - + + src/core/docker/store-container-stats.ts - -store-container-stats.ts + +store-container-stats.ts src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts - - + + src/core/docker/store-host-stats.ts->src/core/utils/logger.ts - - + + src/core/docker/store-host-stats.ts->src/core/database/repository.ts - - + + src/core/docker/store-host-stats.ts->src/typings/docker.ts - - + + src/core/docker/store-host-stats.ts->src/core/docker/client.ts - - + + src/typings/dockerode.ts - -dockerode.ts + +dockerode.ts src/core/docker/store-host-stats.ts->src/typings/dockerode.ts - - + + src/core/docker/store-container-stats.ts->src/core/utils/logger.ts - - + + src/core/docker/store-container-stats.ts->src/core/database/repository.ts - - + + src/core/docker/store-container-stats.ts->src/core/docker/client.ts - - + + src/core/utils/calculations.ts - -calculations.ts + +calculations.ts src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts - - + + src/core/plugins/loader.ts - -loader.ts + +loader.ts src/core/plugins/loader.ts->fs - - + + src/core/plugins/loader.ts->path - - + + src/core/plugins/loader.ts->src/core/utils/logger.ts - - + + src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts - - + + src/core/utils/change-me-checker.ts - -change-me-checker.ts + +change-me-checker.ts src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts - - + + src/core/utils/change-me-checker.ts->fs/promises - - + + src/core/utils/change-me-checker.ts->src/core/utils/logger.ts - - + + src/typings/plugin.ts->src/typings/docker.ts - - + + src/core/stacks/controller.ts - -controller.ts + +controller.ts src/core/stacks/controller.ts->src/core/utils/logger.ts - - + + src/core/stacks/controller.ts->src/core/database/repository.ts - - + + src/core/stacks/controller.ts->src/typings/database.ts - - + + src/typings/docker-compose.ts - -docker-compose.ts + +docker-compose.ts src/core/stacks/controller.ts->src/typings/docker-compose.ts - - + + src/core/trpc/index.ts - -index.ts + +index.ts @@ -628,705 +628,699 @@ src/core/trpc/router.ts - -router.ts + +router.ts src/core/trpc/index.ts->src/core/trpc/router.ts - - + + src/core/trpc/procedures/api-config.procedure.ts - -api-config.procedure.ts + +api-config.procedure.ts src/core/trpc/router.ts->src/core/trpc/procedures/api-config.procedure.ts - - + + src/core/trpc/trpc.ts - -trpc.ts + +trpc.ts src/core/trpc/router.ts->src/core/trpc/trpc.ts - - + + src/core/trpc/procedures/docker-manager.procedure.ts - -docker-manager.procedure.ts + +docker-manager.procedure.ts src/core/trpc/router.ts->src/core/trpc/procedures/docker-manager.procedure.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts - -docker-stats.procedure.ts + +docker-stats.procedure.ts src/core/trpc/router.ts->src/core/trpc/procedures/docker-stats.procedure.ts - - + + src/core/trpc/procedures/logs.procedure.ts - -logs.procedure.ts + +logs.procedure.ts src/core/trpc/router.ts->src/core/trpc/procedures/logs.procedure.ts - - + + src/core/trpc/procedures/stacks.procedure.ts - -stacks.procedure.ts + +stacks.procedure.ts src/core/trpc/router.ts->src/core/trpc/procedures/stacks.procedure.ts - - + + src/core/trpc/procedures/api-config.procedure.ts->src/core/utils/logger.ts - - + + src/core/trpc/procedures/api-config.procedure.ts->src/core/database/repository.ts - - + + src/core/trpc/procedures/api-config.procedure.ts->src/typings/database.ts - - + + src/core/trpc/procedures/api-config.procedure.ts->src/core/trpc/trpc.ts - - + + src/core/utils/package-json.ts - -package-json.ts + +package-json.ts src/core/trpc/procedures/api-config.procedure.ts->src/core/utils/package-json.ts - - + + src/core/utils/package-json.ts->package.json - - + + src/core/trpc/procedures/docker-manager.procedure.ts->src/core/utils/logger.ts - - + + src/core/trpc/procedures/docker-manager.procedure.ts->src/core/database/repository.ts - - + + src/core/trpc/procedures/docker-manager.procedure.ts->src/typings/docker.ts - - + + src/core/trpc/procedures/docker-manager.procedure.ts->src/core/trpc/trpc.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/utils/logger.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/database/repository.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/docker.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/docker/client.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/utils/calculations.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/dockerode.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/trpc/trpc.ts - - + + src/core/trpc/procedures/logs.procedure.ts->src/core/utils/logger.ts - - + + src/core/trpc/procedures/logs.procedure.ts->src/core/database/repository.ts - - + + src/core/trpc/procedures/logs.procedure.ts->src/core/trpc/trpc.ts - - + + src/core/trpc/procedures/stacks.procedure.ts->src/core/utils/logger.ts - - + + src/core/trpc/procedures/stacks.procedure.ts->src/core/database/repository.ts - - + + src/core/trpc/procedures/stacks.procedure.ts->src/core/stacks/controller.ts - - + + src/core/trpc/procedures/stacks.procedure.ts->src/core/trpc/trpc.ts - - + + src/routes/live-logs.ts->src/core/utils/logger.ts - - - - + + + + src/routes/live-logs.ts->src/typings/websocket.ts - - - - - -stream - - -stream - - - - - -src/typings/websocket.ts->stream - - + + src/core/utils/response-handler.ts - -response-handler.ts + +response-handler.ts src/core/utils/response-handler.ts->src/core/utils/logger.ts - - + + src/typings/elysiajs.ts - -elysiajs.ts + +elysiajs.ts src/core/utils/response-handler.ts->src/typings/elysiajs.ts - - + + src/index.ts - -index.ts + +index.ts src/index.ts->src/core/utils/logger.ts - - + + src/index.ts->src/core/database/repository.ts - - + + src/index.ts->src/typings/database.ts - - + + src/index.ts->src/core/docker/monitor.ts - - + + src/index.ts->src/core/docker/scheduler.ts - - + + src/index.ts->src/core/plugins/loader.ts - - + + src/index.ts->src/core/trpc/index.ts - - + + src/index.ts->src/routes/live-logs.ts - - + + src/middleware/auth.ts - -auth.ts + +auth.ts src/index.ts->src/middleware/auth.ts - - + + src/routes/stacks.ts - -stacks.ts + +stacks.ts src/index.ts->src/routes/stacks.ts - - + + src/routes/utils.ts - -utils.ts + +utils.ts src/index.ts->src/routes/utils.ts - - + + src/routes/api-config.ts - -api-config.ts + +api-config.ts src/index.ts->src/routes/api-config.ts - - + + src/routes/docker-manager.ts - -docker-manager.ts + +docker-manager.ts src/index.ts->src/routes/docker-manager.ts - - + + src/routes/docker-stats.ts - -docker-stats.ts + +docker-stats.ts src/index.ts->src/routes/docker-stats.ts - - + + src/routes/docker-websocket.ts - -docker-websocket.ts + +docker-websocket.ts src/index.ts->src/routes/docker-websocket.ts - - + + src/routes/logs.ts - -logs.ts + +logs.ts src/index.ts->src/routes/logs.ts - - + + src/middleware/auth.ts->src/core/utils/logger.ts - - + + src/middleware/auth.ts->src/core/database/repository.ts - - + + src/middleware/auth.ts->src/typings/database.ts - - + + src/middleware/auth.ts->src/typings/elysiajs.ts - - + + src/routes/stacks.ts->src/core/utils/logger.ts - - + + src/routes/stacks.ts->src/core/database/repository.ts - - + + src/routes/stacks.ts->src/core/stacks/controller.ts - - + + src/routes/stacks.ts->src/core/utils/response-handler.ts - - + + src/routes/utils.ts->src/core/utils/package-json.ts - - + + src/routes/utils.ts->src/core/utils/response-handler.ts - - + + src/routes/api-config.ts->src/core/utils/logger.ts - - + + src/routes/api-config.ts->src/core/database/repository.ts - - + + src/routes/api-config.ts->src/typings/database.ts - - + + src/routes/api-config.ts->src/core/plugins/plugin-manager.ts - - + + src/routes/api-config.ts->src/core/utils/package-json.ts - - + + src/routes/api-config.ts->src/core/utils/response-handler.ts - - + + src/routes/api-config.ts->src/middleware/auth.ts - - + + src/routes/docker-manager.ts->src/core/utils/logger.ts - - + + src/routes/docker-manager.ts->src/core/database/repository.ts - - + + src/routes/docker-manager.ts->src/core/utils/response-handler.ts - - + + src/routes/docker-stats.ts->src/core/utils/logger.ts - - + + src/routes/docker-stats.ts->src/core/database/repository.ts - - + + src/routes/docker-stats.ts->src/typings/docker.ts - - + + src/routes/docker-stats.ts->src/core/docker/client.ts - - + + src/routes/docker-stats.ts->src/core/utils/calculations.ts - - + + src/routes/docker-stats.ts->src/typings/dockerode.ts - - + + src/routes/docker-stats.ts->src/core/utils/response-handler.ts - - + + src/routes/docker-websocket.ts->src/core/utils/logger.ts - - + + src/routes/docker-websocket.ts->src/core/database/repository.ts - - + + src/routes/docker-websocket.ts->src/core/docker/client.ts - - + + src/routes/docker-websocket.ts->src/core/utils/calculations.ts - - + + src/routes/docker-websocket.ts->src/core/utils/response-handler.ts - - + + + + + +stream + + +stream + + src/routes/docker-websocket.ts->stream - - + + src/routes/logs.ts->src/core/utils/logger.ts - - + + src/routes/logs.ts->src/core/database/repository.ts - - + + From 26fac15b4ed540965c7b00cee6ad52d7c5036a38 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Thu, 27 Mar 2025 10:36:20 +0100 Subject: [PATCH 224/324] Fix: WebSocket connection show all containers --- src/core/utils/calculations.ts | 17 +++++++++++++++-- src/routes/docker-websocket.ts | 20 ++++++++++---------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/core/utils/calculations.ts b/src/core/utils/calculations.ts index 9428bf9..3f12f95 100644 --- a/src/core/utils/calculations.ts +++ b/src/core/utils/calculations.ts @@ -19,14 +19,27 @@ const calculateCpuPercent = (stats: Docker.ContainerStats): number => { return 0; } - return (cpuDelta / systemDelta) * 100; + const data = (cpuDelta / systemDelta) * 100; + + if (data === null) { + return 0; + } + + return data; }; const calculateMemoryUsage = (stats: Docker.ContainerStats): number => { if (stats == null) { return 0.0; } - return (stats.memory_stats.usage / stats.memory_stats.limit) * 100; + + const data = (stats.memory_stats.usage / stats.memory_stats.limit) * 100; + + if (data === null) { + return 0; + } + + return data; }; export { calculateCpuPercent, calculateMemoryUsage }; diff --git a/src/routes/docker-websocket.ts b/src/routes/docker-websocket.ts index c4444b4..b3a6c52 100644 --- a/src/routes/docker-websocket.ts +++ b/src/routes/docker-websocket.ts @@ -38,9 +38,9 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( const docker = getDockerClient(host); await docker.ping(); - const containers = await docker.listContainers(); + const containers = await docker.listContainers({ all: true }); logger.debug( - `Found ${containers.length} containers on ${host.name} (id: ${host.id})` + `Found ${containers.length} containers on ${host.name} (id: ${host.id})`, ); for (const containerInfo of containers) { @@ -73,9 +73,9 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( image: containerInfo.Image, status: containerInfo.Status, state: containerInfo.State, - cpuUsage: calculateCpuPercent(stats), - memoryUsage: calculateMemoryUsage(stats), - }) + cpuUsage: calculateCpuPercent(stats) || 0, + memoryUsage: calculateMemoryUsage(stats) || 0, + }), ); } catch (error) { logger.error(`Parse error: ${error}`); @@ -89,7 +89,7 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( hostId: host.name, containerId: containerInfo.Id, error: `Stats stream error: ${error}`, - }) + }), ); }); } @@ -102,9 +102,9 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( { headers: {} }, error as string, "Docker connection failed", - 500 - ) - ) + 500, + ), + ), ); } }, @@ -129,5 +129,5 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( }); connectionStreams.delete(ws); }, - } + }, ); From c1f577d2c4da561363ecb67530664c3de43e803c Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Sat, 29 Mar 2025 19:39:19 +0100 Subject: [PATCH 225/324] Feat: More IDs and smaller changes --- docker/docker-compose.dev.yaml | 2 +- src/core/database/repository.ts | 96 +++++++++++++++-------------- src/core/docker/store-host-stats.ts | 19 +++++- src/core/stacks/controller.ts | 47 +++++++++----- src/routes/docker-manager.ts | 44 ++++++++++--- src/routes/stacks.ts | 42 ++++++------- src/typings/database.ts | 1 + src/typings/docker-compose.ts | 1 + src/typings/docker.ts | 5 +- 9 files changed, 163 insertions(+), 94 deletions(-) diff --git a/docker/docker-compose.dev.yaml b/docker/docker-compose.dev.yaml index cca8f34..878d1f0 100644 --- a/docker/docker-compose.dev.yaml +++ b/docker/docker-compose.dev.yaml @@ -42,7 +42,7 @@ services: - VOLUMES=1 #optional sqlite-web: - container_name: qlite-web + container_name: sqlite-web image: ghcr.io/coleifer/sqlite-web:latest ports: - 8080:8080 diff --git a/src/core/database/repository.ts b/src/core/database/repository.ts index a6dfade..1c920a7 100644 --- a/src/core/database/repository.ts +++ b/src/core/database/repository.ts @@ -20,7 +20,8 @@ export const dbFunctions = { ); CREATE TABLE IF NOT EXISTS stacks_config ( - name TEXT PRIMARY KEY NOT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, version INTEGER NOT NULL, custom BOOLEAN NOT NULL, source TEXT NOT NULL, @@ -38,20 +39,21 @@ export const dbFunctions = { ); CREATE TABLE IF NOT EXISTS host_stats ( - hostId TEXT PRIMARY KEY NOT NULL, - dockerVersion TEXT NOT NULL, - apiVersion TEXT NOT NULL, - os TEXT NOT NULL, - architecture TEXT NOT NULL, - totalMemory INTEGER NOT NULL, - totalCPU INTEGER NOT NULL, - labels TEXT NOT NULL, - containers INTEGER NOT NULL, - containersRunning INTEGER NOT NULL, - containersStopped INTEGER NOT NULL, - containersPaused INTEGER NOT NULL, - images INTEGER NOT NULL - ); + hostId INTEGER PRIMARY KEY NOT NULL, + hostName TEXT NOT NULL, + dockerVersion TEXT NOT NULL, + apiVersion TEXT NOT NULL, + os TEXT NOT NULL, + architecture TEXT NOT NULL, + totalMemory INTEGER NOT NULL, + totalCPU INTEGER NOT NULL, + labels TEXT NOT NULL, + containers INTEGER NOT NULL, + containersRunning INTEGER NOT NULL, + containersStopped INTEGER NOT NULL, + containersPaused INTEGER NOT NULL, + images INTEGER NOT NULL + ); CREATE TABLE IF NOT EXISTS container_stats ( id TEXT NOT NULL, @@ -88,7 +90,7 @@ export const dbFunctions = { const stmt = db.prepare( ` INSERT INTO config (keep_data_for, fetching_interval, api_key) VALUES (7, 5, "changeme") - ` + `, ); stmt.run(); } @@ -101,7 +103,7 @@ export const dbFunctions = { const stmt = db.prepare( ` INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?) - ` + `, ); stmt.run("Localhost", "localhost:2375", false); } @@ -139,7 +141,7 @@ export const dbFunctions = { logger.error("Invalid parameter types for addDockerHost"); throw new TypeError("Invalid parameter types for addDockerHost"); } - } + }, ); }, @@ -154,7 +156,7 @@ export const dbFunctions = { `); return stmt.all() as DockerHost[]; }, - () => {} + () => {}, ); }, @@ -162,7 +164,7 @@ export const dbFunctions = { level: string, message: string, file_name: string, - line: number + line: number, ) => { if ( typeof level !== "string" || @@ -192,7 +194,7 @@ export const dbFunctions = { `); return stmt.all(); }, - () => {} + () => {}, ); }, @@ -213,7 +215,7 @@ export const dbFunctions = { logger.error("Level parameter must be a string"); throw new TypeError("Level parameter must be a string"); } - } + }, ); }, @@ -230,7 +232,7 @@ export const dbFunctions = { host.hostAddress, host.secure, host.name, - String(host.id) + String(host.id), ); }, () => { @@ -243,7 +245,7 @@ export const dbFunctions = { logger.error("Invalid parameter types for updateDockerHost"); throw new TypeError("Invalid parameter types for updateDockerHost"); } - } + }, ); }, @@ -262,7 +264,7 @@ export const dbFunctions = { logger.error("Invalid parameter type for deleteDockerHost"); throw new TypeError("id parameter must be a number"); } - } + }, ); }, @@ -275,7 +277,7 @@ export const dbFunctions = { `); return stmt.run(); }, - () => {} + () => {}, ); }, @@ -294,14 +296,14 @@ export const dbFunctions = { logger.error("Invalid parameter type for clearLogsByLevel"); throw new TypeError("Level parameter must be a string"); } - } + }, ); }, updateConfig( fetching_interval: number, keep_data_for: number, - api_key: string + api_key: string, ) { return executeDbOperation( "Update Config", @@ -322,7 +324,7 @@ export const dbFunctions = { logger.error("Invalid parameter types for updateConfig"); throw new TypeError("Invalid parameter types for updateConfig"); } - } + }, ); }, @@ -336,7 +338,7 @@ export const dbFunctions = { `); return stmt.all(); }, - () => {} + () => {}, ); }, @@ -361,7 +363,7 @@ export const dbFunctions = { logger.error("Invalid parameter type for deleteOldData"); throw new TypeError("Days parameter must be a number"); } - } + }, ); }, @@ -373,7 +375,7 @@ export const dbFunctions = { status: string, state: string, cpu_usage: number, - memory_usage: number + memory_usage: number, ) { return executeDbOperation( "Add Container Stats", @@ -390,7 +392,7 @@ export const dbFunctions = { status, state, cpu_usage, - memory_usage + memory_usage, ); }, () => { @@ -407,7 +409,7 @@ export const dbFunctions = { logger.error("Invalid parameter types for addContainerStats"); throw new TypeError("Invalid parameter types for addContainerStats"); } - } + }, ); }, @@ -419,6 +421,7 @@ export const dbFunctions = { const stmt = db.prepare(` INSERT INTO host_stats ( hostId, + hostName, dockerVersion, apiVersion, os, @@ -432,7 +435,7 @@ export const dbFunctions = { containersPaused, images ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(hostId) DO UPDATE SET dockerVersion = excluded.dockerVersion, apiVersion = excluded.apiVersion, @@ -449,6 +452,7 @@ export const dbFunctions = { `); return stmt.run( stats.hostId, + stats.hostName, stats.dockerVersion, stats.apiVersion, stats.os, @@ -460,10 +464,10 @@ export const dbFunctions = { stats.containersRunning, stats.containersStopped, stats.containersPaused, - stats.images + stats.images, ); }, - () => {} + () => {}, ); }, @@ -492,10 +496,10 @@ export const dbFunctions = { stack_config.container_count, stack_config.stack_prefix, stack_config.automatic_reboot_on_error, - stack_config.image_updates + stack_config.image_updates, ); }, - () => {} + () => {}, ); }, @@ -510,21 +514,21 @@ export const dbFunctions = { `); return stmt.all(); }, - () => {} + () => {}, ); }, - deleteStack(name: string) { + deleteStack(id: number) { return executeDbOperation( "Delete Stack", () => { const stmt = db.prepare(` DELETE FROM stacks_config - WHERE name = ?; + WHERE id = ?; `); - return stmt.run(name); + return stmt.run(id); }, - () => {} + () => {}, ); }, @@ -552,10 +556,10 @@ export const dbFunctions = { stack_config.stack_prefix, stack_config.automatic_reboot_on_error, stack_config.image_updates, - stack_config.name + stack_config.name, ); }, - () => {} + () => {}, ); }, }; diff --git a/src/core/docker/store-host-stats.ts b/src/core/docker/store-host-stats.ts index f8cd352..a7fe6e1 100644 --- a/src/core/docker/store-host-stats.ts +++ b/src/core/docker/store-host-stats.ts @@ -4,6 +4,15 @@ import { DockerHost, HostStats } from "~/typings/docker"; import { getDockerClient } from "~/core/docker/client"; import { DockerInfo } from "~/typings/dockerode"; +function getHostByName(hostName: string): DockerHost { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + const foundHost = hosts.find((host) => host.name === hostName); + if (!foundHost) { + throw new Error(`Host ${hostName} not found`); + } + return foundHost; +} + async function storeHostData() { try { const hosts = dbFunctions.getDockerHosts() as DockerHost[]; @@ -22,7 +31,6 @@ async function storeHostData() { } let hostStats: DockerInfo; - let stats: HostStats; try { hostStats = await docker.info(); } catch (error) { @@ -32,9 +40,16 @@ async function storeHostData() { ); } + const hostId = getHostByName(host.name).id; + + if (!hostId) { + throw new Error(`Host "${host.name}" not found`); + } + try { const stats: HostStats = { - hostId: host.name, + hostId: hostId, + hostName: host.name, dockerVersion: hostStats.ServerVersion, apiVersion: hostStats.Driver, os: hostStats.OperatingSystem, diff --git a/src/core/stacks/controller.ts b/src/core/stacks/controller.ts index f4af21a..1f2e034 100644 --- a/src/core/stacks/controller.ts +++ b/src/core/stacks/controller.ts @@ -6,17 +6,17 @@ import type { Stack, ComposeSpec } from "~/typings/docker-compose"; import type { stacks_config } from "~/typings/database"; async function runStackCommand( - stack_name: string, + stack_id: number, command: (cwd: string) => Promise, action: string, ): Promise { try { - const stack = { name: stack_name }; + const stack = { id: stack_id }; const stackPath = await getStackPath(stack as Stack); return await command(stackPath); } catch (error: any) { throw new Error( - `Error while ${action} stack "${stack_name}": ${error.message || error}`, + `Error while ${action} stack "${stack_id}": ${error.message || error}`, ); } } @@ -54,6 +54,7 @@ export async function deployStack( const resolvedPrefix = stack_prefix ?? ""; const stack_config: stacks_config = { + id: 0, name: name, version: version, source, @@ -87,10 +88,10 @@ export async function deployStack( } } -export async function stopStack(stack_name: string): Promise { +export async function stopStack(stack_id: number): Promise { try { await runStackCommand( - stack_name, + stack_id, (cwd) => DockerCompose.downAll({ cwd }), "stopping", ); @@ -101,10 +102,10 @@ export async function stopStack(stack_name: string): Promise { } } -export async function startStack(stack_name: string): Promise { +export async function startStack(stack_id: number): Promise { try { await runStackCommand( - stack_name, + stack_id, (cwd) => DockerCompose.upAll({ cwd }), "starting", ); @@ -115,10 +116,10 @@ export async function startStack(stack_name: string): Promise { } } -export async function pullStackImages(stack_name: string): Promise { +export async function pullStackImages(stack_id: number): Promise { try { await runStackCommand( - stack_name, + stack_id, (cwd) => DockerCompose.pullAll({ cwd }), "pulling images for", ); @@ -129,10 +130,10 @@ export async function pullStackImages(stack_name: string): Promise { } } -export async function restartStack(stack_name: string): Promise { +export async function restartStack(stack_id: number): Promise { try { await runStackCommand( - stack_name, + stack_id, (cwd) => DockerCompose.restartAll({ cwd }), "restarting", ); @@ -143,10 +144,10 @@ export async function restartStack(stack_name: string): Promise { } } -export async function getStackStatus(stack_name: string): Promise { +export async function getStackStatus(stack_id: number): Promise { try { return await runStackCommand( - stack_name, + stack_id, async (cwd) => { const rawStatus = await DockerCompose.ps({ cwd }); return rawStatus.data.services.reduce((acc: any, service: any) => { @@ -163,6 +164,24 @@ export async function getStackStatus(stack_name: string): Promise { } } +export async function removeStack(stack_id: number): Promise { + try { + await runStackCommand( + stack_id, + async (cwd) => { + await DockerCompose.down({ cwd }); + }, + "removing", + ); + + dbFunctions.deleteStack(stack_id); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + throw new Error(errorMsg); + } +} + export async function getAllStacksStatus(): Promise> { try { const stacks = dbFunctions.getStacks() as stacks_config[]; @@ -170,7 +189,7 @@ export async function getAllStacksStatus(): Promise> { const statusResults = await Promise.all( stacks.map(async (stack) => { const status = await runStackCommand( - stack.name, + stack.id, async (cwd) => { const rawStatus = await DockerCompose.ps({ cwd }); return rawStatus.data.services.reduce((acc: any, service: any) => { diff --git a/src/routes/docker-manager.ts b/src/routes/docker-manager.ts index dcf97a7..d3e88c7 100644 --- a/src/routes/docker-manager.ts +++ b/src/routes/docker-manager.ts @@ -2,6 +2,7 @@ import { Elysia, t } from "elysia"; import { dbFunctions } from "~/core/database/repository"; import { logger } from "~/core/utils/logger"; import { responseHandler } from "~/core/utils/response-handler"; +import { DockerHost } from "~/typings/docker"; export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) .post( @@ -9,13 +10,13 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) async ({ set, body }) => { try { set.headers["Content-Type"] = "application/json"; - dbFunctions.addDockerHost(body); + dbFunctions.addDockerHost(body as DockerHost); return responseHandler.ok(set, `Added docker host (${body.name})`); } catch (error: unknown) { return responseHandler.error( set, "Error adding docker Host", - error as string + error as string, ); } }, @@ -29,7 +30,7 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) hostAddress: t.String(), secure: t.Boolean(), }), - } + }, ) .post( @@ -37,12 +38,13 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) async ({ set, body }) => { try { set.status = 200; - return dbFunctions.updateDockerHost(body); + dbFunctions.updateDockerHost(body); + return responseHandler.ok(set, `Updated docker host (${body.id})`); } catch (error) { return responseHandler.error( set, error as string, - "Failed to update host" + "Failed to update host", ); } }, @@ -57,7 +59,7 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) hostAddress: t.String(), secure: t.Boolean(), }), - } + }, ) .get( @@ -72,7 +74,7 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) return responseHandler.error( set, error as string, - "Failed to retrieve hosts" + "Failed to retrieve hosts", ); } }, @@ -81,5 +83,31 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) tags: ["Management"], description: "Returns an Array of Host-config-objects", }, - } + }, + ) + + .delete( + "/hosts/:id", + async ({ set, params }) => { + try { + set.status = 200; + dbFunctions.deleteDockerHost(params.id); + return responseHandler.ok(set, `Deleted docker host (${params.id})`); + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to delete host", + ); + } + }, + { + detail: { + tags: ["Management"], + description: "Delete an existing host", + }, + params: t.Object({ + id: t.Number(), + }), + }, ); diff --git a/src/routes/stacks.ts b/src/routes/stacks.ts index d212ba4..41304da 100644 --- a/src/routes/stacks.ts +++ b/src/routes/stacks.ts @@ -48,18 +48,18 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) body.automatic_reboot_on_error, isCustom, image_updates, - body.stack_prefix + body.stack_prefix, ); logger.info(`Deployed Stack (${body.name})`); return responseHandler.ok( set, - `Stack ${body.name} deployed successfully` + `Stack ${body.name} deployed successfully`, ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error deploying stack" + "Error deploying stack", ); } }, @@ -79,7 +79,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) source: t.String(), stack_prefix: t.Optional(t.String()), }), - } + }, ) .post( "/start", @@ -92,13 +92,13 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) logger.info(`Started Stack (${body.stack})`); return responseHandler.ok( set, - `Stack ${body.stack} started successfully` + `Stack ${body.stack} started successfully`, ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error starting stack" + "Error starting stack", ); } }, @@ -107,7 +107,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) body: t.Object({ stack: t.Any(), }), - } + }, ) .post( "/stop", @@ -120,13 +120,13 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) logger.info(`Stopped Stack (${body.stack})`); return responseHandler.ok( set, - `Stack ${body.stack} stopped successfully` + `Stack ${body.stack} stopped successfully`, ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error stopping stack" + "Error stopping stack", ); } }, @@ -135,7 +135,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) body: t.Object({ stack: t.Any(), }), - } + }, ) .post( "/restart", @@ -148,13 +148,13 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) logger.info(`Restarted Stack (${body.stack})`); return responseHandler.ok( set, - `Stack ${body.stack} restarted successfully` + `Stack ${body.stack} restarted successfully`, ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error restarting stack" + "Error restarting stack", ); } }, @@ -163,7 +163,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) body: t.Object({ stack: t.Any(), }), - } + }, ) .post( "/pull-images", @@ -176,13 +176,13 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) logger.info(`Pulled Stack images (${body.stack})`); return responseHandler.ok( set, - `Images for stack ${body.stack} pulled successfully` + `Images for stack ${body.stack} pulled successfully`, ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error pulling images" + "Error pulling images", ); } }, @@ -194,7 +194,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) body: t.Object({ stack: t.Any(), }), - } + }, ) .get( "/status", @@ -206,7 +206,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) status = await getStackStatus(query.stack_name); res = responseHandler.ok( set, - `Stack ${query.stack_name} status retrieved successfully` + `Stack ${query.stack_name} status retrieved successfully`, ); logger.info("Fetched Stack status"); } else { @@ -219,7 +219,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) return responseHandler.error( set, error.message || error, - "Error getting stack status" + "Error getting stack status", ); } }, @@ -232,7 +232,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) query: t.Object({ stack_name: t.Any(), }), - } + }, ) .get( "/", @@ -245,7 +245,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) return responseHandler.error( set, error.message || error, - "Error getting stacks" + "Error getting stacks", ); } }, @@ -254,5 +254,5 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) tags: ["Stacks"], description: "Returns an Array of Stack-config-objects", }, - } + }, ); diff --git a/src/typings/database.ts b/src/typings/database.ts index cebda60..96319e9 100644 --- a/src/typings/database.ts +++ b/src/typings/database.ts @@ -5,6 +5,7 @@ interface config { } interface stacks_config { + id: number; name: string; version: number; custom: boolean; diff --git a/src/typings/docker-compose.ts b/src/typings/docker-compose.ts index 9b23c2e..9067aba 100644 --- a/src/typings/docker-compose.ts +++ b/src/typings/docker-compose.ts @@ -3,6 +3,7 @@ export interface Stack { name: string; version: number; source: string; + id?: number; } export interface ComposeSpec { diff --git a/src/typings/docker.ts b/src/typings/docker.ts index e6701f6..0d759ba 100644 --- a/src/typings/docker.ts +++ b/src/typings/docker.ts @@ -2,7 +2,7 @@ interface DockerHost { name: string; hostAddress: string; secure: boolean | number; - id?: number; + id: number; } interface ContainerInfo { @@ -17,7 +17,8 @@ interface ContainerInfo { } interface HostStats { - hostId: string; + hostName: string; + hostId: number; dockerVersion: string; apiVersion: string; os: string; From ac0b4309b9d9d5a1977177186183943972ae99da Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Sat, 29 Mar 2025 23:33:11 +0100 Subject: [PATCH 226/324] Fix: Refactor and Stacks now have IDs ! --- src/core/database/config.ts | 55 ++++ src/core/database/containerStats.ts | 34 +++ src/core/database/database.ts | 92 +++++++ src/core/database/dockerHosts.ts | 57 ++++ src/core/database/helper.ts | 13 +- src/core/database/hostStats.ts | 45 +++ src/core/database/index.ts | 19 ++ src/core/database/logs.ts | 73 +++++ src/core/database/stacks.ts | 72 +++++ src/core/docker/monitor.ts | 10 +- src/core/docker/scheduler.ts | 2 +- src/core/docker/store-container-stats.ts | 24 +- src/core/docker/store-host-stats.ts | 2 +- src/core/plugins/loader.ts | 23 +- src/core/stacks/controller.ts | 40 ++- .../trpc/procedures/api-config.procedure.ts | 2 +- .../procedures/docker-manager.procedure.ts | 2 +- .../trpc/procedures/docker-stats.procedure.ts | 14 +- src/core/trpc/procedures/logs.procedure.ts | 2 +- src/core/trpc/procedures/stacks.procedure.ts | 2 +- src/core/utils/logger.ts | 259 +++++++++++------- src/index.ts | 49 ++-- src/middleware/auth.ts | 8 +- src/routes/api-config.ts | 20 +- src/routes/docker-manager.ts | 2 +- src/routes/docker-stats.ts | 26 +- src/routes/docker-websocket.ts | 2 +- src/routes/logs.ts | 2 +- src/routes/stacks.ts | 80 ++++-- src/tests/cleanup.ts | 2 +- 30 files changed, 819 insertions(+), 214 deletions(-) create mode 100644 src/core/database/config.ts create mode 100644 src/core/database/containerStats.ts create mode 100644 src/core/database/database.ts create mode 100644 src/core/database/dockerHosts.ts create mode 100644 src/core/database/hostStats.ts create mode 100644 src/core/database/index.ts create mode 100644 src/core/database/logs.ts create mode 100644 src/core/database/stacks.ts diff --git a/src/core/database/config.ts b/src/core/database/config.ts new file mode 100644 index 0000000..126682e --- /dev/null +++ b/src/core/database/config.ts @@ -0,0 +1,55 @@ +import { db } from "./database"; +import { executeDbOperation } from "./helper"; + +const stmt = { + update: db.prepare( + `UPDATE config SET fetching_interval = ?, keep_data_for = ?, api_key = ?`, + ), + select: db.prepare( + `SELECT keep_data_for, fetching_interval, api_key FROM config`, + ), + deleteOld: db.prepare( + `DELETE FROM container_stats WHERE timestamp < datetime('now', '-' || ? || ' days')`, + ), + deleteOldLogs: db.prepare( + `DELETE FROM backend_log_entries WHERE timestamp < datetime('now', '-' || ? || ' days')`, + ), +}; + +export function updateConfig( + fetching_interval: number, + keep_data_for: number, + api_key: string, +) { + return executeDbOperation( + "Update Config", + () => stmt.update.run(fetching_interval, keep_data_for, api_key), + () => { + if ( + typeof fetching_interval !== "number" || + typeof keep_data_for !== "number" + ) { + throw new TypeError("Invalid config parameters"); + } + }, + ); +} + +export function getConfig() { + return executeDbOperation("Get Config", () => stmt.select.all()); +} + +export function deleteOldData(days: number) { + return executeDbOperation( + "Delete Old Data", + () => { + db.transaction(() => { + stmt.deleteOld.run(days); + stmt.deleteOldLogs.run(days); + })(); + }, + () => { + if (typeof days !== "number") throw new TypeError("Invalid days type"); + }, + ); +} diff --git a/src/core/database/containerStats.ts b/src/core/database/containerStats.ts new file mode 100644 index 0000000..d0fb197 --- /dev/null +++ b/src/core/database/containerStats.ts @@ -0,0 +1,34 @@ +import { db } from "./database"; +import { executeDbOperation } from "./helper"; + +const stmt = db.prepare(` + INSERT INTO container_stats (id, hostId, name, image, status, state, cpu_usage, memory_usage) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) +`); + +export function addContainerStats( + id: string, + hostId: string, + name: string, + image: string, + status: string, + state: string, + cpu_usage: number, + memory_usage: number, +) { + return executeDbOperation( + "Add Container Stats", + () => + stmt.run(id, hostId, name, image, status, state, cpu_usage, memory_usage), + () => { + if ( + typeof id !== "string" || + typeof hostId !== "string" || + typeof cpu_usage !== "number" || + typeof memory_usage !== "number" + ) { + throw new TypeError("Invalid container stats parameters"); + } + }, + ); +} diff --git a/src/core/database/database.ts b/src/core/database/database.ts new file mode 100644 index 0000000..5173e44 --- /dev/null +++ b/src/core/database/database.ts @@ -0,0 +1,92 @@ +import { Database } from "bun:sqlite"; + +export const db = new Database("dockstatapi.db", { strict: true }); +db.exec("PRAGMA journal_mode = WAL;"); + +export function init() { + db.exec(` + CREATE TABLE IF NOT EXISTS backend_log_entries ( + timestamp STRING NOT NULL, + level TEXT NOT NULL, + message TEXT NOT NULL, + file TEXT NOT NULL, + line NUMBER NOT NULL + ); + + CREATE TABLE IF NOT EXISTS stacks_config ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + version INTEGER NOT NULL, + custom BOOLEAN NOT NULL, + source TEXT NOT NULL, + container_count INTEGER NOT NULL, + stack_prefix TEXT NOT NULL, + automatic_reboot_on_error BOOLEAN NOT NULL, + image_updates BOOLEAN NOT NULL + ); + + CREATE TABLE IF NOT EXISTS docker_hosts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + hostAddress TEXT NOT NULL, + secure BOOLEAN NOT NULL + ); + + CREATE TABLE IF NOT EXISTS host_stats ( + hostId INTEGER PRIMARY KEY NOT NULL, + hostName TEXT NOT NULL, + dockerVersion TEXT NOT NULL, + apiVersion TEXT NOT NULL, + os TEXT NOT NULL, + architecture TEXT NOT NULL, + totalMemory INTEGER NOT NULL, + totalCPU INTEGER NOT NULL, + labels TEXT NOT NULL, + containers INTEGER NOT NULL, + containersRunning INTEGER NOT NULL, + containersStopped INTEGER NOT NULL, + containersPaused INTEGER NOT NULL, + images INTEGER NOT NULL + ); + + CREATE TABLE IF NOT EXISTS container_stats ( + id TEXT NOT NULL, + hostId TEXT NOT NULL, + name TEXT NOT NULL, + image TEXT NOT NULL, + status TEXT NOT NULL, + state TEXT NOT NULL, + cpu_usage FLOAT NOT NULL, + memory_usage FLOAT NOT NULL, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP + ); + + CREATE TABLE IF NOT EXISTS config ( + keep_data_for NUMBER NOT NULL, + fetching_interval NUMBER NOT NULL, + api_key TEXT NOT NULL + ); + `); + + const configRow = db + .prepare(`SELECT COUNT(*) AS count FROM config`) + .get() as { count: number }; + + if (configRow.count === 0) { + db.prepare( + `INSERT INTO config (keep_data_for, fetching_interval, api_key) VALUES (7, 5, "changeme")`, + ).run(); + } + + const hostRow = db + .prepare(`SELECT COUNT(*) AS count FROM docker_hosts`) + .get() as { count: number }; + + if (hostRow.count === 0) { + db.prepare( + `INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)`, + ).run("Localhost", "localhost:2375", false); + } +} + +init(); diff --git a/src/core/database/dockerHosts.ts b/src/core/database/dockerHosts.ts new file mode 100644 index 0000000..bdaf2d1 --- /dev/null +++ b/src/core/database/dockerHosts.ts @@ -0,0 +1,57 @@ +import { db } from "./database"; +import { executeDbOperation } from "./helper"; +import type { DockerHost } from "~/typings/docker"; + +const stmt = { + insert: db.prepare( + `INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)`, + ), + selectAll: db.prepare( + `SELECT id, name, hostAddress, secure FROM docker_hosts ORDER BY id DESC`, + ), + update: db.prepare( + `UPDATE docker_hosts SET hostAddress = ?, secure = ?, name = ? WHERE id = ?`, + ), + delete: db.prepare(`DELETE FROM docker_hosts WHERE id = ?`), +}; + +export function addDockerHost(host: DockerHost) { + return executeDbOperation( + "Add Docker Host", + () => stmt.insert.run(host.name, host.hostAddress, host.secure), + () => { + if (!host.name || !host.hostAddress) + throw new Error("Missing required fields"); + if (typeof host.secure !== "boolean") + throw new TypeError("Invalid secure type"); + }, + ); +} + +export function getDockerHosts(): DockerHost[] { + return executeDbOperation( + "Get Docker Hosts", + () => stmt.selectAll.all() as DockerHost[], + ); +} + +export function updateDockerHost(host: DockerHost) { + return executeDbOperation( + "Update Docker Host", + () => stmt.update.run(host.hostAddress, host.secure, host.name, host.id), + () => { + if (!host.id || typeof host.id !== "number") + throw new Error("Invalid host ID"); + }, + ); +} + +export function deleteDockerHost(id: number) { + return executeDbOperation( + "Delete Docker Host", + () => stmt.delete.run(id), + () => { + if (typeof id !== "number") throw new TypeError("Invalid ID type"); + }, + ); +} diff --git a/src/core/database/helper.ts b/src/core/database/helper.ts index 753bc89..3edbdaa 100644 --- a/src/core/database/helper.ts +++ b/src/core/database/helper.ts @@ -1,17 +1,22 @@ -import { logger } from "../utils/logger"; +import { logger } from "~/core/utils/logger"; export function executeDbOperation( label: string, operation: () => T, - validate?: () => void + validate?: () => void, + dontLog?: boolean, ): T { const startTime = Date.now(); - logger.debug(`__task__ __db__ ${label} ⏳`); + if (dontLog !== true) { + logger.debug(`__task__ __db__ ${label} ⏳`); + } if (validate) { validate(); } const result = operation(); const duration = Date.now() - startTime; - logger.debug(`__task__ __db__ ${label} ✔️ (${duration}ms)`); + if (dontLog !== true) { + logger.debug(`__task__ __db__ ${label} ✔️ (${duration}ms)`); + } return result; } diff --git a/src/core/database/hostStats.ts b/src/core/database/hostStats.ts new file mode 100644 index 0000000..04aa426 --- /dev/null +++ b/src/core/database/hostStats.ts @@ -0,0 +1,45 @@ +import { db } from "./database"; +import { executeDbOperation } from "./helper"; +import type { HostStats } from "~/typings/docker"; + +const stmt = db.prepare(` + INSERT INTO host_stats ( + hostId, hostName, dockerVersion, apiVersion, os, architecture, + totalMemory, totalCPU, labels, containers, containersRunning, + containersStopped, containersPaused, images + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(hostId) DO UPDATE SET + dockerVersion = excluded.dockerVersion, + apiVersion = excluded.apiVersion, + os = excluded.os, + architecture = excluded.architecture, + totalMemory = excluded.totalMemory, + totalCPU = excluded.totalCPU, + labels = excluded.labels, + containers = excluded.containers, + containersRunning = excluded.containersRunning, + containersStopped = excluded.containersStopped, + containersPaused = excluded.containersPaused, + images = excluded.images +`); + +export function updateHostStats(stats: HostStats) { + return executeDbOperation("Update Host Stats", () => + stmt.run( + stats.hostId, + stats.hostName, + stats.dockerVersion, + stats.apiVersion, + stats.os, + stats.architecture, + stats.totalMemory, + stats.totalCPU, + JSON.stringify(stats.labels), + stats.containers, + stats.containersRunning, + stats.containersStopped, + stats.containersPaused, + stats.images, + ), + ); +} diff --git a/src/core/database/index.ts b/src/core/database/index.ts new file mode 100644 index 0000000..3559fe9 --- /dev/null +++ b/src/core/database/index.ts @@ -0,0 +1,19 @@ +import { init } from "~/core/database/database"; + +init(); + +import * as dockerHosts from "~/core/database/dockerHosts"; +import * as logs from "~/core/database/logs"; +import * as config from "~/core/database/config"; +import * as containerStats from "~/core/database/containerStats"; +import * as hostStats from "~/core/database/hostStats"; +import * as stacks from "~/core/database/stacks"; + +export const dbFunctions = { + ...dockerHosts, + ...logs, + ...config, + ...containerStats, + ...hostStats, + ...stacks, +}; diff --git a/src/core/database/logs.ts b/src/core/database/logs.ts new file mode 100644 index 0000000..26ce2c1 --- /dev/null +++ b/src/core/database/logs.ts @@ -0,0 +1,73 @@ +import { logStreamData } from "~/typings/websocket"; +import { db } from "./database"; +import { executeDbOperation } from "./helper"; + +const stmt = { + insert: db.prepare( + `INSERT INTO backend_log_entries (timestamp, level, message, file, line) VALUES (?, ?, ?, ?, ?)`, + ), + selectAll: db.prepare( + `SELECT timestamp, level, message, file, line FROM backend_log_entries ORDER BY timestamp DESC`, + ), + selectByLevel: db.prepare( + `SELECT timestamp, level, message, file, line FROM backend_log_entries WHERE level = ?`, + ), + deleteAll: db.prepare(`DELETE FROM backend_log_entries`), + deleteByLevel: db.prepare(`DELETE FROM backend_log_entries WHERE level = ?`), +}; + +export function addLogEntry(data: logStreamData) { + return executeDbOperation( + "Add Log Entry", + () => + stmt.insert.run( + data.level, + data.timestamp, + data.message, + data.file, + data.line, + ), + () => { + if ( + typeof data.level !== "string" || + typeof data.timestamp !== "string" || + typeof data.message !== "string" || + typeof data.file !== "string" || + typeof data.line !== "number" + ) { + throw new TypeError( + `Invalid log entry parameters ${data.file} ${data.line} ${data.message} ${data}`, + ); + } + }, + true, + ); +} + +export function getAllLogs() { + return executeDbOperation("Get All Logs", () => stmt.selectAll.all()); +} + +export function getLogsByLevel(level: string) { + return executeDbOperation( + "Get Logs By Level", + () => stmt.selectByLevel.all(level), + () => { + if (typeof level !== "string") throw new TypeError("Invalid level type"); + }, + ); +} + +export function clearAllLogs() { + return executeDbOperation("Clear All Logs", () => stmt.deleteAll.run()); +} + +export function clearLogsByLevel(level: string) { + return executeDbOperation( + "Clear Logs By Level", + () => stmt.deleteByLevel.run(level), + () => { + if (typeof level !== "string") throw new TypeError("Invalid level type"); + }, + ); +} diff --git a/src/core/database/stacks.ts b/src/core/database/stacks.ts new file mode 100644 index 0000000..0491625 --- /dev/null +++ b/src/core/database/stacks.ts @@ -0,0 +1,72 @@ +import { Stack } from "~/typings/docker-compose"; +import { db } from "./database"; +import { executeDbOperation } from "./helper"; +import type { stacks_config } from "~/typings/database"; + +const stmt = { + insert: db.prepare(` + INSERT INTO stacks_config ( + name, version, custom, source, container_count, + stack_prefix, automatic_reboot_on_error, image_updates + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `), + selectAll: db.prepare(` + SELECT id, name, version, custom, source, container_count, stack_prefix, + automatic_reboot_on_error, image_updates + FROM stacks_config + ORDER BY name DESC + `), + update: db.prepare(` + UPDATE stacks_config SET + version = ?, custom = ?, source = ?, container_count = ?, + stack_prefix = ?, automatic_reboot_on_error = ?, image_updates = ? + WHERE name = ? + `), + delete: db.prepare(`DELETE FROM stacks_config WHERE id = ?`), +}; + +export function addStack(stack: stacks_config) { + return executeDbOperation("Add Stack", () => + stmt.insert.run( + stack.name, + stack.version, + stack.custom, + stack.source, + stack.container_count, + stack.stack_prefix, + stack.automatic_reboot_on_error, + stack.image_updates, + ), + ); +} + +export function getStacks() { + return executeDbOperation("Get Stacks", () => + stmt.selectAll.all(), + ) as Stack[]; +} + +export function deleteStack(id: number) { + return executeDbOperation( + "Delete Stack", + () => stmt.delete.run(id), + () => { + if (typeof id !== "number") throw new TypeError("Invalid stack ID"); + }, + ); +} + +export function updateStack(stack: stacks_config) { + return executeDbOperation("Update Stack", () => + stmt.update.run( + stack.version, + stack.custom, + stack.source, + stack.container_count, + stack.stack_prefix, + stack.automatic_reboot_on_error, + stack.image_updates, + stack.name, + ), + ); +} diff --git a/src/core/docker/monitor.ts b/src/core/docker/monitor.ts index 1257326..eadd5f6 100644 --- a/src/core/docker/monitor.ts +++ b/src/core/docker/monitor.ts @@ -1,5 +1,5 @@ import type { DockerHost } from "~/typings/docker"; -import { dbFunctions } from "~/core/database/repository"; +import { dbFunctions } from "~/core/database"; import { getDockerClient } from "~/core/docker/client"; import { logger } from "~/core/utils/logger"; import { pluginManager } from "../plugins/plugin-manager"; @@ -12,7 +12,7 @@ export async function monitorDockerEvents() { try { hosts = dbFunctions.getDockerHosts(); logger.debug( - `Retrieved ${hosts.length} Docker host(s) for event monitoring.` + `Retrieved ${hosts.length} Docker host(s) for event monitoring.`, ); } catch (error: unknown) { logger.error(`Error retrieving Docker hosts: ${(error as Error).message}`); @@ -58,7 +58,7 @@ async function startFor(host: DockerHost) { event = JSON.parse(line); } catch (parseErr: any) { logger.error( - `Failed to parse event from host ${host.name}: ${parseErr.message}` + `Failed to parse event from host ${host.name}: ${parseErr.message}`, ); continue; } @@ -113,7 +113,7 @@ async function startFor(host: DockerHost) { break; default: logger.debug( - `Unhandled container event "${action}" on host ${host.name}` + `Unhandled container event "${action}" on host ${host.name}`, ); } } @@ -132,7 +132,7 @@ async function startFor(host: DockerHost) { }); } catch (streamErr: any) { logger.error( - `Failed to start events stream for host ${host.name}: ${streamErr.message}` + `Failed to start events stream for host ${host.name}: ${streamErr.message}`, ); } } diff --git a/src/core/docker/scheduler.ts b/src/core/docker/scheduler.ts index e4adf9c..2d7fae1 100644 --- a/src/core/docker/scheduler.ts +++ b/src/core/docker/scheduler.ts @@ -1,5 +1,5 @@ import storeContainerData from "~/core/docker/store-container-stats"; -import { dbFunctions } from "~/core/database/repository"; +import { dbFunctions } from "~/core/database"; import { config } from "~/typings/database"; import { logger } from "~/core/utils/logger"; import storeHostData from "~/core/docker//store-host-stats"; diff --git a/src/core/docker/store-container-stats.ts b/src/core/docker/store-container-stats.ts index 6bcc168..69c12e0 100644 --- a/src/core/docker/store-container-stats.ts +++ b/src/core/docker/store-container-stats.ts @@ -1,5 +1,5 @@ import { getDockerClient } from "~/core/docker/client"; -import { dbFunctions } from "~/core/database/repository"; +import { dbFunctions } from "~/core/database"; import Docker from "dockerode"; import { calculateCpuPercent, @@ -23,7 +23,7 @@ async function storeContainerData() { } catch (error) { const errMsg = error instanceof Error ? error.message : String(error); throw new Error( - `Failed to ping docker host "${host.name}": ${errMsg}` + `Failed to ping docker host "${host.name}": ${errMsg}`, ); } @@ -33,7 +33,7 @@ async function storeContainerData() { } catch (error) { const errMsg = error instanceof Error ? error.message : String(error); throw new Error( - `Failed to list containers on host "${host.name}": ${errMsg}` + `Failed to list containers on host "${host.name}": ${errMsg}`, ); } @@ -52,20 +52,20 @@ async function storeContainerData() { error instanceof Error ? error.message : String(error); return reject( new Error( - `Failed to get stats for container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}": ${errMsg}` - ) + `Failed to get stats for container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}": ${errMsg}`, + ), ); } if (!stats) { return reject( new Error( - `No stats returned for container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}".` - ) + `No stats returned for container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}".`, + ), ); } resolve(stats); }); - } + }, ); dbFunctions.addContainerStats( @@ -76,18 +76,18 @@ async function storeContainerData() { containerInfo.Status, containerInfo.State, calculateCpuPercent(stats), - calculateMemoryUsage(stats) + calculateMemoryUsage(stats), ); } catch (error) { const errMsg = error instanceof Error ? error.message : String(error); throw new Error( - `Error processing container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}": ${errMsg}` + `Error processing container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}": ${errMsg}`, ); } - }) + }), ); - }) + }), ); } catch (error) { const errMsg = error instanceof Error ? error.message : String(error); diff --git a/src/core/docker/store-host-stats.ts b/src/core/docker/store-host-stats.ts index a7fe6e1..9536bc1 100644 --- a/src/core/docker/store-host-stats.ts +++ b/src/core/docker/store-host-stats.ts @@ -1,5 +1,5 @@ import { logger } from "~/core/utils/logger"; -import { dbFunctions } from "~/core/database/repository"; +import { dbFunctions } from "~/core/database"; import { DockerHost, HostStats } from "~/typings/docker"; import { getDockerClient } from "~/core/docker/client"; import { DockerInfo } from "~/typings/dockerode"; diff --git a/src/core/plugins/loader.ts b/src/core/plugins/loader.ts index db77bed..6fad398 100644 --- a/src/core/plugins/loader.ts +++ b/src/core/plugins/loader.ts @@ -8,19 +8,34 @@ export async function loadPlugins(pluginDir: string) { const pluginPath = path.join(process.cwd(), pluginDir); logger.debug(`Loading plugins (${pluginPath})`); + if (!fs.existsSync(pluginPath)) { - return; + throw new Error(`Failed to check plugin directory`); } + logger.debug(`Plugin directory exists`); let pluginCount = 0; - const files = fs.readdirSync(pluginPath); + let files; + try { + files = fs.readdirSync(pluginPath); + logger.debug(`Found ${files.length} files in plugin directory`); + } catch (error) { + throw new Error(`Failed to read plugin-directory: ${error}`); + } + + if (!files) { + logger.info(`No plugins found in ${pluginPath}`); + return; + } for (const file of files) { if (!file.endsWith(".plugin.ts")) { - continue - }; + logger.debug(`Skipping non-plugin file: ${file}`); + continue; + } const absolutePath = path.join(pluginPath, file); + logger.info(`Loading plugin: ${absolutePath}`); try { await checkFileForChangeMe(absolutePath); const module = await import(absolutePath); diff --git a/src/core/stacks/controller.ts b/src/core/stacks/controller.ts index 1f2e034..7f1aeb9 100644 --- a/src/core/stacks/controller.ts +++ b/src/core/stacks/controller.ts @@ -1,9 +1,21 @@ -import { dbFunctions } from "../database/repository"; +import { dbFunctions } from "~/core/database"; import YAML from "yaml"; -import { logger } from "../utils/logger"; +import { logger } from "~/core/utils/logger"; import DockerCompose from "docker-compose"; import type { Stack, ComposeSpec } from "~/typings/docker-compose"; import type { stacks_config } from "~/typings/database"; +import { rm } from "node:fs/promises"; +import { ErrorLike } from "bun"; + +async function getStackName(stack_id: number): Promise { + logger.debug(`Fetching stack name for id ${stack_id}`); + const stacks = dbFunctions.getStacks(); + const stack = stacks.find((stack) => Number(stack.id) === Number(stack_id)); + if (!stack) { + throw new Error(`Stack with id ${stack_id} not found`); + } + return stack.name; +} async function runStackCommand( stack_id: number, @@ -11,7 +23,7 @@ async function runStackCommand( action: string, ): Promise { try { - const stack = { id: stack_id }; + const stack = { id: stack_id, name: await getStackName(stack_id) }; const stackPath = await getStackPath(stack as Stack); return await command(stackPath); } catch (error: any) { @@ -174,7 +186,25 @@ export async function removeStack(stack_id: number): Promise { "removing", ); + const stackName = await getStackName(stack_id); + + const stack = { + id: stack_id, + }; + + const stackPath = await getStackPath(stack as Stack); + + try { + await rm("stackPath", { recursive: true }); + } catch (error: any) { + if (error.code === "ENOENT") { + console.log("Directory doesn't exist"); + } else { + throw error; + } + } dbFunctions.deleteStack(stack_id); + logger.info(`Stack ${stackName} (${stack_id}) removed successfully`); } catch (error: unknown) { const errorMsg = error instanceof Error ? error.message : String(error); logger.error(errorMsg); @@ -184,12 +214,12 @@ export async function removeStack(stack_id: number): Promise { export async function getAllStacksStatus(): Promise> { try { - const stacks = dbFunctions.getStacks() as stacks_config[]; + const stacks = dbFunctions.getStacks(); const statusResults = await Promise.all( stacks.map(async (stack) => { const status = await runStackCommand( - stack.id, + stack.id as number, async (cwd) => { const rawStatus = await DockerCompose.ps({ cwd }); return rawStatus.data.services.reduce((acc: any, service: any) => { diff --git a/src/core/trpc/procedures/api-config.procedure.ts b/src/core/trpc/procedures/api-config.procedure.ts index 6b3b248..f517568 100644 --- a/src/core/trpc/procedures/api-config.procedure.ts +++ b/src/core/trpc/procedures/api-config.procedure.ts @@ -1,4 +1,4 @@ -import { dbFunctions } from "~/core/database/repository"; +import { dbFunctions } from "~/core/database"; import { logger } from "~/core/utils/logger"; import { version, diff --git a/src/core/trpc/procedures/docker-manager.procedure.ts b/src/core/trpc/procedures/docker-manager.procedure.ts index 7621a5a..a91e769 100644 --- a/src/core/trpc/procedures/docker-manager.procedure.ts +++ b/src/core/trpc/procedures/docker-manager.procedure.ts @@ -1,4 +1,4 @@ -import { dbFunctions } from "~/core/database/repository"; +import { dbFunctions } from "~/core/database"; import { logger } from "~/core/utils/logger"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; diff --git a/src/core/trpc/procedures/docker-stats.procedure.ts b/src/core/trpc/procedures/docker-stats.procedure.ts index 017e880..0a28929 100644 --- a/src/core/trpc/procedures/docker-stats.procedure.ts +++ b/src/core/trpc/procedures/docker-stats.procedure.ts @@ -1,5 +1,5 @@ import Docker from "dockerode"; -import { dbFunctions } from "~/core/database/repository"; +import { dbFunctions } from "~/core/database"; import { getDockerClient } from "~/core/docker/client"; import { calculateCpuPercent, @@ -47,7 +47,7 @@ export const dockerStatsProcedure = router({ code: "INTERNAL_SERVER_ERROR", message: "Error fetching container stats", cause: error, - }) + }), ); } if (!stats) { @@ -55,12 +55,12 @@ export const dockerStatsProcedure = router({ new TRPCError({ code: "NOT_FOUND", message: "No stats available", - }) + }), ); } resolve(stats as Docker.ContainerStats); }); - } + }, ); containers.push({ @@ -76,16 +76,16 @@ export const dockerStatsProcedure = router({ } catch (containerError) { logger.error( "Error fetching container stats", - containerError + containerError, ); } - }) + }), ); logger.debug(`Fetched stats for ${host.name}`); } catch (hostError) { logger.error("Error fetching containers for host", hostError); } - }) + }), ); logger.debug("Fetched all containers across all hosts"); diff --git a/src/core/trpc/procedures/logs.procedure.ts b/src/core/trpc/procedures/logs.procedure.ts index 520a2cb..b15fc9f 100644 --- a/src/core/trpc/procedures/logs.procedure.ts +++ b/src/core/trpc/procedures/logs.procedure.ts @@ -1,4 +1,4 @@ -import { dbFunctions } from "~/core/database/repository"; +import { dbFunctions } from "~/core/database"; import { logger } from "~/core/utils/logger"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; diff --git a/src/core/trpc/procedures/stacks.procedure.ts b/src/core/trpc/procedures/stacks.procedure.ts index db7a85c..495fd32 100644 --- a/src/core/trpc/procedures/stacks.procedure.ts +++ b/src/core/trpc/procedures/stacks.procedure.ts @@ -1,4 +1,4 @@ -import { dbFunctions } from "~/core/database/repository"; +import { dbFunctions } from "~/core/database"; import { logger } from "~/core/utils/logger"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts index fce5909..6c0c5ee 100644 --- a/src/core/utils/logger.ts +++ b/src/core/utils/logger.ts @@ -1,135 +1,200 @@ import { createLogger, format, transports } from "winston"; +import type { TransformableInfo } from "logform"; import path from "path"; import chalk, { ChalkInstance } from "chalk"; -import { dbFunctions } from "../database/repository"; +import { dbFunctions } from "~/core/database"; import wrapAnsi from "wrap-ansi"; import { logToClients } from "~/routes/live-logs"; -import { logStreamData } from "~/typings/websocket"; +import type { logStreamData } from "~/typings/websocket"; + +const padNewlines = process.env.PAD_NEW_LINES !== "false"; + +type LogLevel = + | "error" + | "warn" + | "info" + | "debug" + | "verbose" + | "silly" + | "task" + | "ut"; + +interface CustomTransformableInfo extends TransformableInfo { + file: string; + line: number; +} + +type LogStreamData = Omit & { + message: string; +}; const ansiRegex = /\x1B\[[0-?9;]*[mG]/g; -// Change to false here if dont want the spacing on a wrapped line -const padNewlines: boolean = process.env.PAD_NEW_LINES === "true" || true; - -const fileLineFormat = format((info) => { +const formatTerminalMessage = (message: string, prefix: string): string => { try { - const stack = new Error().stack?.split("\n"); - if (stack) { - for (let i = 2; i < stack.length; i++) { - const line = stack[i].trim(); - // Exclude lines from node_modules or the current file - if ( - !line.includes("node_modules") && - !line.includes(path.basename(__filename)) - ) { - const matches = line.match(/\(?(.+):(\d+):(\d+)\)?$/); - if (matches) { - info.file = path.basename(matches[1]); - info.line = parseInt(matches[2], 10); - break; - } - } - } - } - } catch (err) { - // Ignore errors during stack trace extraction - } - return info; -}); + const cleanPrefix = prefix.replace(ansiRegex, ""); + const maxWidth = process.stdout.columns || 80; + const wrapWidth = Math.max(maxWidth - cleanPrefix.length - 3, 20); -const formatTerminalMessage = (message: string, prefixLength: number) => { - const maxWidth = process.stdout.columns || 80; - const wrapWidth = maxWidth - prefixLength - 15; + if (!padNewlines) return message; - if (padNewlines) { - const wrapped = wrapAnsi(chalk.gray(message), wrapWidth, { + const wrapped = wrapAnsi(message, wrapWidth, { trim: true, hard: true, + wordWrap: true, }); return wrapped .split("\n") - .map((line, i) => (i === 0 ? line : " ".repeat(prefixLength) + line)) + .map((line, index) => { + return index === 0 ? line : `${" ".repeat(cleanPrefix.length)}${line}`; + }) .join("\n"); + } catch (error) { + console.error("Error formatting terminal message:", error); + return message; + } +}; + +const levelColors: Record = { + error: chalk.red.bold, + warn: chalk.yellow.bold, + info: chalk.green.bold, + debug: chalk.blue.bold, + verbose: chalk.cyan.bold, + silly: chalk.magenta.bold, + task: chalk.cyan.bold, + ut: chalk.hex("#9D00FF"), +}; + +const handleWebSocketLog = ( + level: string, + timestamp: string, + message: string, + file: string, + line: number, +) => { + try { + const data = { + timestamp, + level: level, + message: message, + file: file, + line: line, + }; + + logToClients(data); + } catch (error) { + console.error( + `WebSocket logging failed: ${error instanceof Error ? error.message : error}`, + ); + } +}; + +const handleDatabaseLog = ( + level: string, + timestamp: string, + message: string, + file: string, + line: number, +): void => { + try { + const data = { + timestamp, + level, + message, + file: file, + line: line, + }; + + dbFunctions.addLogEntry(data); + } catch (error) { + console.error( + `Database logging failed: ${error instanceof Error ? error.message : error}`, + ); } - return message; }; +// Main logger export const logger = createLogger({ level: process.env.LOG_LEVEL || "debug", format: format.combine( format.timestamp({ format: "DD/MM HH:mm:ss" }), - fileLineFormat(), - format.printf(({ timestamp, level, message, file, line }) => { - const levelColors: Record = { - error: chalk.red.bold, - warn: chalk.yellow.bold, - info: chalk.green.bold, - debug: chalk.blue.bold, - verbose: chalk.cyan.bold, - silly: chalk.magenta.bold, - task: chalk.cyan.bold, - ut: chalk.hex("#9D00FF"), - }; - - if ((message as string).startsWith("__task__")) { - message = (message as string).replaceAll("__task__", "").trimStart(); - level = "task"; - if ((message as string).startsWith("__db__")) { - message = (message as string).replaceAll("__db__", "").trimStart(); - message = `${chalk.magenta("DB")} ${message}`; + format((info) => { + const stack = new Error().stack?.split("\n"); + let file = "unknown"; + let line = 0; + + if (stack) { + for (let i = 2; i < stack.length; i++) { + const lineStr = stack[i].trim(); + if ( + !lineStr.includes("node_modules") && + !lineStr.includes(path.basename(__filename)) + ) { + const matches = lineStr.match(/\(?(.+):(\d+):(\d+)\)?$/); + if (matches) { + file = path.basename(matches[1]); + line = parseInt(matches[2], 10); + break; + } + } } } - - if ((message as string).startsWith("__UT__")) { - message = (message as string).replaceAll("__UT__", "").trimStart(); - level = "ut"; + return { ...info, file, line }; + })(), + format.printf((info) => { + const { timestamp, level, message, file, line } = + info as CustomTransformableInfo; + let processedLevel = level as LogLevel; + let processedMessage = String(message); + + if (processedMessage.startsWith("__task__")) { + processedMessage = processedMessage + .replace(/__task__/g, "") + .trimStart(); + processedLevel = "task"; + if (processedMessage.startsWith("__db__")) { + processedMessage = processedMessage + .replace(/__db__/g, "") + .trimStart(); + processedMessage = `${chalk.magenta("DB")} ${processedMessage}`; + } + } else if (processedMessage.startsWith("__UT__")) { + processedMessage = processedMessage.replace(/__UT__/g, "").trimStart(); + processedLevel = "ut"; } - if ((file as string).includes("plugin.ts")) { - message = `[ ${chalk.greenBright("Plugin")} ] ${message}`; + if (file.endsWith("plugin.ts")) { + processedMessage = `[ ${chalk.greenBright("Plugin")} ] ${processedMessage}`; } - const logStreamData: logStreamData = { - timestamp: timestamp as string, - level: level as string, - message: (message as string).replace(ansiRegex, ""), - file: file as string, - line: line as number, - }; - - logToClients(logStreamData); - - const paddedLevel = level.toUpperCase().padEnd(5); - const coloredLevel = (levelColors[level] || chalk.white)(paddedLevel); - const coloredContext = chalk.cyan(`${file as string}:${line as number}`); + const paddedLevel = processedLevel.toUpperCase().padEnd(5); + const coloredLevel = (levelColors[processedLevel] || chalk.white)( + paddedLevel, + ); + const coloredContext = chalk.cyan(`${file}:${line}`); const coloredTimestamp = chalk.yellow(timestamp); - if (process.env.NODE_ENV !== "dev") { - return `${coloredLevel} [ ${coloredTimestamp} ] - ${chalk.gray( - message, - )} - [ ${coloredContext} ]`; - } - const prefix = `${paddedLevel} [ ${timestamp} ] - `; - const prefixLength = prefix.length; - const formattedMessage = formatTerminalMessage( - message as string, - prefixLength + const formattedMessage = padNewlines + ? formatTerminalMessage(processedMessage, prefix) + : processedMessage; + + handleDatabaseLog( + coloredTimestamp.replace(ansiRegex, "").trim(), + coloredLevel.replace(ansiRegex, "").trim(), + processedMessage.replace(ansiRegex, "").trim(), + file.trim(), + line, + ); + handleWebSocketLog( + coloredLevel.replace(ansiRegex, "").trim(), + coloredTimestamp.replace(ansiRegex, "").trim(), + processedMessage.replace(ansiRegex, "").trim(), + file.trim(), + line, ); - - try { - dbFunctions.addLogEntry( - (level as string).replace(ansiRegex, ""), - (message as string).replace(ansiRegex, ""), - (file as string).replace(ansiRegex, ""), - line as number, - ); - } catch (error) { - // Use console.error to avoid recursive logging - console.error(`Error inserting log into DB: ${String(error)}`); - process.abort(); - } return `${coloredLevel} [ ${coloredTimestamp} ] - ${formattedMessage} - [ ${coloredContext} ]`; }), diff --git a/src/index.ts b/src/index.ts index f96da4b..c7d7f11 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ +import { dbFunctions } from "~/core/database"; import { swagger } from "@elysiajs/swagger"; import { Elysia } from "elysia"; -import { dbFunctions } from "~/core/database/repository"; import { loadPlugins } from "~/core/plugins/loader"; import { logger } from "~/core/utils/logger"; import { dockerRoutes } from "~/routes/docker-manager"; @@ -20,7 +20,8 @@ import { liveLogs } from "./routes/live-logs"; import { utilRoutes } from "./routes/utils"; console.log(""); -dbFunctions.init(); + +logger.info("Starting DockStatAPI"); export const DockStatAPI = new Elysia() .use(staticPlugin()) @@ -68,7 +69,7 @@ export const DockStatAPI = new Elysia() }, ], }, - }) + }), ) .onBeforeHandle(async (context) => { const { path, request, set } = context; @@ -108,8 +109,17 @@ export const DockStatAPI = new Elysia() async function startServer() { try { - await loadPlugins("./src/plugins"); - await setSchedules(); + try { + await loadPlugins("./src/plugins"); + } catch (error) { + throw new Error(`Failed to load plugins: ${error}`); + } + + try { + await setSchedules(); + } catch (error) { + throw new Error(`Failed to set schedules: ${error}`); + } monitorDockerEvents().catch((error) => { logger.error(`Monitoring Error: ${error}`); @@ -120,22 +130,27 @@ async function startServer() { if (apiKey === "changeme") { logger.warn( - "Default API Key of 'changeme' detected. Please change your API Key via the `/config/update` route!" + "Default API Key of 'changeme' detected. Please change your API Key via the `/config/update` route!", ); } - DockStatAPI.listen(3000, ({ hostname, port }) => { - console.log("----- [ ############## ]"); - logger.info(`DockStatAPI is running at http://${hostname}:${port}`); - logger.info( - `Swagger API Documentation available at http://${hostname}:${port}/swagger` - ); - logger.info( - `tRPC Endpoint available at: http://${hostname}:${port}/trpc` - ); - }); + try { + DockStatAPI.listen(3000, ({ hostname, port }) => { + console.log("----- [ ############## ]"); + logger.info(`DockStatAPI is running at http://${hostname}:${port}`); + logger.info( + `Swagger API Documentation available at http://${hostname}:${port}/swagger`, + ); + logger.info( + `tRPC Endpoint available at: http://${hostname}:${port}/trpc`, + ); + }); + } catch (error) { + logger.error("Failed to start server:", error); + process.exit(1); + } } catch (error) { - logger.error("Failed to start server:", error); + logger.error("Error while starting server:", error); process.exit(1); } } diff --git a/src/middleware/auth.ts b/src/middleware/auth.ts index 2672f88..17ae2c6 100644 --- a/src/middleware/auth.ts +++ b/src/middleware/auth.ts @@ -1,4 +1,4 @@ -import { dbFunctions } from "~/core/database/repository"; +import { dbFunctions } from "~/core/database"; import { logger } from "~/core/utils/logger"; import { config } from "~/typings/database"; import { set } from "~/typings/elysiajs"; @@ -16,7 +16,7 @@ export async function hashApiKey(apiKey: string): Promise { async function validateApiKeyHash( providedKey: string, - storedHash: string + storedHash: string, ): Promise { logger.debug("Validating API key hash"); try { @@ -30,7 +30,7 @@ async function validateApiKeyHash( } async function getApiKeyFromDb( - apiKey: string + apiKey: string, ): Promise<{ hash: string } | null> { const dbApiKey = (dbFunctions.getConfig() as config[])[0].api_key; logger.debug(`Querying database for API key: ${apiKey}`); @@ -44,7 +44,7 @@ export async function validateApiKey(request: Request, set: set) { if (process.env.NODE_ENV != "production") { logger.warn( - "API Key validation deactivated, since running in development mode" + "API Key validation deactivated, since running in development mode", ); return { apiKey }; } else if (!apiKey) { diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index 17ea352..2059b0e 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -1,5 +1,5 @@ import { Elysia, t } from "elysia"; -import { dbFunctions } from "~/core/database/repository"; +import { dbFunctions } from "~/core/database"; import { logger } from "~/core/utils/logger"; import { responseHandler } from "~/core/utils/response-handler"; import { config } from "~/typings/database"; @@ -32,7 +32,7 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) return responseHandler.error( set, error as string, - "Error getting the DockStatAPI config" + "Error getting the DockStatAPI config", ); } }, @@ -41,7 +41,7 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) tags: ["Management"], description: "Returns DockStatAPI's config", }, - } + }, ) .get( "/plugins", @@ -52,11 +52,11 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) return responseHandler.error( set, error as string, - "Error getting all registered plugins" + "Error getting all registered plugins", ); } }, - { detail: { tags: ["Management"], description: "List all Plugin Names" } } + { detail: { tags: ["Management"], description: "List all Plugin Names" } }, ) .post( "/update", @@ -67,14 +67,14 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) dbFunctions.updateConfig( fetching_interval, keep_data_for, - await hashApiKey(api_key) + await hashApiKey(api_key), ); return responseHandler.ok(set, "Updated DockStatAPI config"); } catch (error) { return responseHandler.error( set, "Error updating the DockStatAPI config", - error as string + error as string, ); } }, @@ -88,7 +88,7 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) tags: ["Management"], description: "Update the current DockStatAPI config", }, - } + }, ) .get( "/package", @@ -110,7 +110,7 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) return responseHandler.error( set, error as string, - "Error while reading package.json" + "Error while reading package.json", ); } }, @@ -119,5 +119,5 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) tags: ["Management"], description: "Returns relevant information about the package.json", }, - } + }, ); diff --git a/src/routes/docker-manager.ts b/src/routes/docker-manager.ts index d3e88c7..e10f8c3 100644 --- a/src/routes/docker-manager.ts +++ b/src/routes/docker-manager.ts @@ -1,5 +1,5 @@ import { Elysia, t } from "elysia"; -import { dbFunctions } from "~/core/database/repository"; +import { dbFunctions } from "~/core/database"; import { logger } from "~/core/utils/logger"; import { responseHandler } from "~/core/utils/response-handler"; import { DockerHost } from "~/typings/docker"; diff --git a/src/routes/docker-stats.ts b/src/routes/docker-stats.ts index c86f688..58bebcf 100644 --- a/src/routes/docker-stats.ts +++ b/src/routes/docker-stats.ts @@ -1,6 +1,6 @@ import Docker from "dockerode"; import { Elysia } from "elysia"; -import { dbFunctions } from "~/core/database/repository"; +import { dbFunctions } from "~/core/database"; import { getDockerClient } from "~/core/docker/client"; import { calculateCpuPercent, @@ -29,7 +29,7 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) return responseHandler.error( set, pingError as string, - "Docker host connection failed" + "Docker host connection failed", ); } @@ -47,19 +47,19 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) set, reject, "An error occurred", - error + error, ); } if (!stats) { return responseHandler.reject( set, reject, - "No stats available" + "No stats available", ); } resolve(stats); }); - } + }, ); containers.push({ @@ -75,16 +75,16 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) } catch (containerError) { logger.error( "Error fetching container stats,", - containerError + containerError, ); } - }) + }), ); logger.debug(`Fetched stats for ${host.name}`); } catch (hostError) { logger.error("Error fetching containers for host,", hostError); } - }) + }), ); set.headers["Content-Type"] = "application/json"; @@ -94,7 +94,7 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) return responseHandler.error( set, error as string, - "Failed to retrieve containers" + "Failed to retrieve containers", ); } }, @@ -104,7 +104,7 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) description: "Fetches all Containers and their statistics across all Hosts", }, - } + }, ) .get( @@ -117,7 +117,7 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) if (!host) { return responseHandler.simple_error( set, - `Host (${params.id}) not found` + `Host (${params.id}) not found`, ); } @@ -147,7 +147,7 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) return responseHandler.error( set, error as string, - "Failed to retrieve host config" + "Failed to retrieve host config", ); } }, @@ -156,5 +156,5 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) tags: ["Statistics"], description: "Fetches the Host Stats for a specified Host", }, - } + }, ); diff --git a/src/routes/docker-websocket.ts b/src/routes/docker-websocket.ts index b3a6c52..43292e2 100644 --- a/src/routes/docker-websocket.ts +++ b/src/routes/docker-websocket.ts @@ -1,6 +1,6 @@ import { Elysia } from "elysia"; import type { ElysiaWS } from "elysia/dist/ws"; -import { dbFunctions } from "~/core/database/repository"; +import { dbFunctions } from "~/core/database"; import { getDockerClient } from "~/core/docker/client"; import { calculateCpuPercent, diff --git a/src/routes/logs.ts b/src/routes/logs.ts index f5cf3cb..d828936 100644 --- a/src/routes/logs.ts +++ b/src/routes/logs.ts @@ -1,5 +1,5 @@ import { Elysia } from "elysia"; -import { dbFunctions } from "~/core/database/repository"; +import { dbFunctions } from "~/core/database"; import { logger } from "~/core/utils/logger"; export const backendLogs = new Elysia({ prefix: "/logs" }) diff --git a/src/routes/stacks.ts b/src/routes/stacks.ts index 41304da..2e07a2f 100644 --- a/src/routes/stacks.ts +++ b/src/routes/stacks.ts @@ -8,8 +8,9 @@ import { getStackStatus, startStack, getAllStacksStatus, + removeStack, } from "~/core/stacks/controller"; -import { dbFunctions } from "~/core/database/repository"; +import { dbFunctions } from "~/core/database"; import { logger } from "~/core/utils/logger"; export const stackRoutes = new Elysia({ prefix: "/stacks" }) @@ -85,14 +86,14 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) "/start", async ({ set, body }) => { try { - if (!body.stack) { - throw new Error("Stack needed"); + if (!body.stackId) { + throw new Error("Stack ID needed"); } - await startStack(body.stack); - logger.info(`Started Stack (${body.stack})`); + await startStack(body.stackId); + logger.info(`Started Stack (${body.stackId})`); return responseHandler.ok( set, - `Stack ${body.stack} started successfully`, + `Stack ${body.stackId} started successfully`, ); } catch (error: any) { return responseHandler.error( @@ -105,7 +106,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) { detail: { tags: ["Stacks"], description: "Start a specific Stack" }, body: t.Object({ - stack: t.Any(), + stackId: t.Number(), }), }, ) @@ -113,14 +114,14 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) "/stop", async ({ set, body }) => { try { - if (!body.stack) { + if (!body.stackId) { throw new Error("Stack needed"); } - await stopStack(body.stack); - logger.info(`Stopped Stack (${body.stack})`); + await stopStack(body.stackId); + logger.info(`Stopped Stack (${body.stackId})`); return responseHandler.ok( set, - `Stack ${body.stack} stopped successfully`, + `Stack ${body.stackId} stopped successfully`, ); } catch (error: any) { return responseHandler.error( @@ -133,7 +134,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) { detail: { tags: ["Stacks"], description: "Stop the specified Stack" }, body: t.Object({ - stack: t.Any(), + stackId: t.Number(), }), }, ) @@ -141,14 +142,14 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) "/restart", async ({ set, body }) => { try { - if (!body.stack) { + if (!body.stackId) { throw new Error("Stack needed"); } - await restartStack(body.stack); - logger.info(`Restarted Stack (${body.stack})`); + await restartStack(body.stackId); + logger.info(`Restarted Stack (${body.stackId})`); return responseHandler.ok( set, - `Stack ${body.stack} restarted successfully`, + `Stack ${body.stackId} restarted successfully`, ); } catch (error: any) { return responseHandler.error( @@ -161,7 +162,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) { detail: { tags: ["Stacks"], description: "Restart a whole Stack" }, body: t.Object({ - stack: t.Any(), + stackId: t.Number(), }), }, ) @@ -169,14 +170,14 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) "/pull-images", async ({ set, body }) => { try { - if (!body.stack) { + if (!body.stackId) { throw new Error("Stack needed"); } - await pullStackImages(body.stack); - logger.info(`Pulled Stack images (${body.stack})`); + await pullStackImages(body.stackId); + logger.info(`Pulled Stack images (${body.stackId})`); return responseHandler.ok( set, - `Images for stack ${body.stack} pulled successfully`, + `Images for stack ${body.stackId} pulled successfully`, ); } catch (error: any) { return responseHandler.error( @@ -192,7 +193,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) description: "Runs `docker compose pull` on the provided Stack", }, body: t.Object({ - stack: t.Any(), + stackId: t.Number(), }), }, ) @@ -202,11 +203,11 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) try { let status; let res = {}; - if (query.stack_name) { - status = await getStackStatus(query.stack_name); + if (query.stackId) { + status = await getStackStatus(query.stackId); res = responseHandler.ok( set, - `Stack ${query.stack_name} status retrieved successfully`, + `Stack ${query.stackId} status retrieved successfully`, ); logger.info("Fetched Stack status"); } else { @@ -230,7 +231,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) "Fetches the current status of all containers for a specific Stack or if no Stack name is provided, for all Stacks", }, query: t.Object({ - stack_name: t.Any(), + stackId: t.Number(), }), }, ) @@ -255,4 +256,31 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) description: "Returns an Array of Stack-config-objects", }, }, + ) + + .delete( + "/", + async ({ set, body }) => { + try { + const { stackId } = body; + await removeStack(stackId); + logger.info(`Deleted Stack ${stackId}`); + return responseHandler.ok(set, `Stack ${stackId} deleted successfully`); + } catch (error: any) { + return responseHandler.error( + set, + error.message || error, + "Error deleting stack", + ); + } + }, + { + detail: { + tags: ["Stacks"], + description: "Delete a Stack", + }, + body: t.Object({ + stackId: t.Number(), + }), + }, ); diff --git a/src/tests/cleanup.ts b/src/tests/cleanup.ts index 163419a..20b965b 100644 --- a/src/tests/cleanup.ts +++ b/src/tests/cleanup.ts @@ -1,4 +1,4 @@ -import { dbFunctions } from "~/core/database/repository"; +import { dbFunctions } from "~/core/database"; import type { DockerHost } from "~/typings/docker"; import { findObjectByKey } from "~/core/utils/helpers"; From b01ef420e5277ecbfc6b7402bc59ed6cfa6fb17b Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Sat, 29 Mar 2025 23:59:18 +0100 Subject: [PATCH 227/324] Feat: Better Swagger documentation and minor fix in stack logic --- src/core/stacks/controller.ts | 4 +- src/core/utils/swagger-readme.ts | 64 ++++++++++++++++++++++++++++++++ src/index.ts | 5 ++- src/routes/api-config.ts | 17 +++++++-- src/routes/docker-manager.ts | 12 ++++-- src/routes/docker-stats.ts | 10 +++-- src/routes/logs.ts | 11 +++--- src/routes/stacks.ts | 31 ++++++++++++---- src/routes/utils.ts | 7 ++-- 9 files changed, 129 insertions(+), 32 deletions(-) create mode 100644 src/core/utils/swagger-readme.ts diff --git a/src/core/stacks/controller.ts b/src/core/stacks/controller.ts index 7f1aeb9..838532c 100644 --- a/src/core/stacks/controller.ts +++ b/src/core/stacks/controller.ts @@ -189,13 +189,13 @@ export async function removeStack(stack_id: number): Promise { const stackName = await getStackName(stack_id); const stack = { - id: stack_id, + name: stackName, }; const stackPath = await getStackPath(stack as Stack); try { - await rm("stackPath", { recursive: true }); + await rm(stackPath, { recursive: true }); } catch (error: any) { if (error.code === "ENOENT") { console.log("Directory doesn't exist"); diff --git a/src/core/utils/swagger-readme.ts b/src/core/utils/swagger-readme.ts new file mode 100644 index 0000000..ff30a8b --- /dev/null +++ b/src/core/utils/swagger-readme.ts @@ -0,0 +1,64 @@ +export const swaggerReadme: string = ` +![Docker](https://img.shields.io/badge/Docker-2CA5E0?style=flat&logo=docker&logoColor=white) +![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=flat&logo=typescript&logoColor=white) + +Docker infrastructure management API with real-time monitoring and orchestration capabilities. + +## Key Features + +- **Stack Orchestration** + Deploy/update Docker stacks (compose v3+) with custom configurations +- **Container Monitoring** + Real-time metrics (CPU/RAM/status) across multiple Docker hosts +- **Centralized Logging** + Structured log management with retention policies and filtering +- **Host Management** + Multi-host configuration with connection health checks +- **Plugin System** + Extensible architecture for custom monitoring integrations + +## Installation & Setup + +**Prerequisites**: +- Node.js 18+ +- Docker Engine 23+ +- Bun runtime + +\`\`\`bash +# Clone repo +git clone https://github.com/Its4Nik/DockStatAPI.git +cd DockStatAPI +# Install dependencies +bun install + +# Start development server +bun run dev +\`\`\` + +## Configuration + +**Environment Variables**: +\`\`\`ini +PAD_NEW_LINES=true +NODE_ENV=production +LOG_LEVEL=info +\`\`\` + +## Security + +1. Always use HTTPS in production +2. Rotate API keys regularly +3. Restrict host connections to trusted networks +4. Enable Docker Engine TLS authentication + +## Contributing + +1. Fork repository +2. Create feature branch (\`feat/my-feature\`) +3. Submit PR with detailed description + +**Code Style**: +- TypeScript strict mode +- Elysia framework conventions +- Prettier formatting +`; diff --git a/src/index.ts b/src/index.ts index c7d7f11..5cc5af3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,6 +18,7 @@ import { validateApiKey } from "./middleware/auth"; import { monitorDockerEvents } from "./core/docker/monitor"; import { liveLogs } from "./routes/live-logs"; import { utilRoutes } from "./routes/utils"; +import { swaggerReadme } from "./core/utils/swagger-readme"; console.log(""); @@ -31,8 +32,8 @@ export const DockStatAPI = new Elysia() documentation: { info: { title: "DockStatAPI", - version: "2.1.0", - description: "Docker monitoring API with plugin support", + version: "3.0.0", + description: swaggerReadme, }, components: { securitySchemes: { diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index 2059b0e..9861d3f 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -39,7 +39,8 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) { detail: { tags: ["Management"], - description: "Returns DockStatAPI's config", + description: + "Returns current API configuration including data retention policies and security settings", }, }, ) @@ -56,7 +57,13 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) ); } }, - { detail: { tags: ["Management"], description: "List all Plugin Names" } }, + { + detail: { + tags: ["Management"], + description: + "Lists all active plugins with their registration details and status", + }, + }, ) .post( "/update", @@ -86,7 +93,8 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) }), detail: { tags: ["Management"], - description: "Update the current DockStatAPI config", + description: + "Modifies core API settings including data collection intervals, retention periods, and security credentials", }, }, ) @@ -117,7 +125,8 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) { detail: { tags: ["Management"], - description: "Returns relevant information about the package.json", + description: + "Displays package metadata including dependencies, contributors, and licensing information", }, }, ); diff --git a/src/routes/docker-manager.ts b/src/routes/docker-manager.ts index e10f8c3..635b78b 100644 --- a/src/routes/docker-manager.ts +++ b/src/routes/docker-manager.ts @@ -23,7 +23,8 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) { detail: { tags: ["Management"], - description: "Add a new Host as Monitoring target", + description: + "Registers a new Docker host to the monitoring system with connection details", }, body: t.Object({ name: t.String(), @@ -51,7 +52,8 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) { detail: { tags: ["Management"], - description: "Update an already existing target's config", + description: + "Modifies existing Docker host configuration parameters (name, address, security)", }, body: t.Object({ id: t.Number(), @@ -81,7 +83,8 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) { detail: { tags: ["Management"], - description: "Returns an Array of Host-config-objects", + description: + "Lists all configured Docker hosts with their connection settings", }, }, ) @@ -104,7 +107,8 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) { detail: { tags: ["Management"], - description: "Delete an existing host", + description: + "Removes Docker host from monitoring system and clears associated data", }, params: t.Object({ id: t.Number(), diff --git a/src/routes/docker-stats.ts b/src/routes/docker-stats.ts index 58bebcf..a600af3 100644 --- a/src/routes/docker-stats.ts +++ b/src/routes/docker-stats.ts @@ -64,7 +64,7 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) containers.push({ id: containerInfo.Id, - hostId: host.id as string, + hostId: `${host.id}`, name: containerInfo.Names[0].replace(/^\//, ""), image: containerInfo.Image, status: containerInfo.Status, @@ -102,7 +102,7 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) detail: { tags: ["Statistics"], description: - "Fetches all Containers and their statistics across all Hosts", + "Collects real-time statistics for all Docker containers across monitored hosts, including CPU and memory utilization", }, }, ) @@ -125,7 +125,8 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) const info: DockerInfo = await docker.info(); const config: HostStats = { - hostId: host.name, + hostId: host.id as number, + hostName: host.name, dockerVersion: info.ServerVersion, apiVersion: info.Driver, os: info.OperatingSystem, @@ -154,7 +155,8 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) { detail: { tags: ["Statistics"], - description: "Fetches the Host Stats for a specified Host", + description: + "Provides detailed system metrics and Docker runtime information for specified host", }, }, ); diff --git a/src/routes/logs.ts b/src/routes/logs.ts index d828936..dc7fc37 100644 --- a/src/routes/logs.ts +++ b/src/routes/logs.ts @@ -20,7 +20,8 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) { detail: { tags: ["Management"], - description: "Retrieves all Logs which have been saved in the Database", + description: + "Retrieves complete application log history from persistent storage", }, }, ) @@ -42,7 +43,8 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) { detail: { tags: ["Management"], - description: "Retrieves all Logs with the specified level", + description: + "Filters logs by severity level (debug, info, warn, error, fatal)", }, }, ) @@ -64,7 +66,7 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) { detail: { tags: ["Management"], - description: "Deletes all Logs which are saved in the Database", + description: "Purges all historical log records from the database", }, }, ) @@ -86,8 +88,7 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) { detail: { tags: ["Management"], - description: - "Deletes all Logs with the specified Level inside the Database", + description: "Clears log entries matching specified severity level", }, }, ); diff --git a/src/routes/stacks.ts b/src/routes/stacks.ts index 2e07a2f..eea4160 100644 --- a/src/routes/stacks.ts +++ b/src/routes/stacks.ts @@ -68,7 +68,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) detail: { tags: ["Stacks"], description: - "Deploy a Stack, either with a prebuilt one or provide your own structure", + "Deploys a new Docker stack using a provided compose specification, allowing custom configurations and image updates", }, body: t.Object({ compose_spec: t.Any(), @@ -104,7 +104,11 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) } }, { - detail: { tags: ["Stacks"], description: "Start a specific Stack" }, + detail: { + tags: ["Stacks"], + description: + "Initiates a Docker stack, starting all associated containers", + }, body: t.Object({ stackId: t.Number(), }), @@ -132,7 +136,11 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) } }, { - detail: { tags: ["Stacks"], description: "Stop the specified Stack" }, + detail: { + tags: ["Stacks"], + description: + "Halts a running Docker stack and its containers while preserving configurations", + }, body: t.Object({ stackId: t.Number(), }), @@ -160,7 +168,11 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) } }, { - detail: { tags: ["Stacks"], description: "Restart a whole Stack" }, + detail: { + tags: ["Stacks"], + description: + "Performs full stack restart - stops and restarts all stack components in sequence", + }, body: t.Object({ stackId: t.Number(), }), @@ -190,7 +202,8 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) { detail: { tags: ["Stacks"], - description: "Runs `docker compose pull` on the provided Stack", + description: + "Updates container images for a stack using Docker's pull mechanism (requires stack ID)", }, body: t.Object({ stackId: t.Number(), @@ -228,7 +241,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) detail: { tags: ["Stacks"], description: - "Fetches the current status of all containers for a specific Stack or if no Stack name is provided, for all Stacks", + "Retrieves operational status for either a specific stack (by ID) or all managed stacks", }, query: t.Object({ stackId: t.Number(), @@ -253,7 +266,8 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) { detail: { tags: ["Stacks"], - description: "Returns an Array of Stack-config-objects", + description: + "Lists all registered stacks with their complete configuration details", }, }, ) @@ -277,7 +291,8 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) { detail: { tags: ["Stacks"], - description: "Delete a Stack", + description: + "Permanently removes a stack configuration and cleans up associated resources", }, body: t.Object({ stackId: t.Number(), diff --git a/src/routes/utils.ts b/src/routes/utils.ts index cb8b942..b61e0e7 100644 --- a/src/routes/utils.ts +++ b/src/routes/utils.ts @@ -32,14 +32,15 @@ export const utilRoutes = new Elysia({ prefix: "/utils" }).get( return responseHandler.error( set, error.message || error, - "Error getting DockStatAPI information" + "Error getting DockStatAPI information", ); } }, { detail: { tags: ["Utils"], - description: "Shows general information about DockStatAPI", + description: + "Retrieves DockStatAPI metadata including version, author information, dependencies, and licensing details", }, - } + }, ); From 8ac97e71cc1992d1e1fde9da477b089f7ab070c7 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Sat, 29 Mar 2025 23:01:01 +0000 Subject: [PATCH 228/324] Update dependency graphs --- dependency-graph.dot | 84 ++- dependency-graph.mmd | 336 +++++---- dependency-graph.svg | 1646 ++++++++++++++++++++++++------------------ 3 files changed, 1183 insertions(+), 883 deletions(-) diff --git a/dependency-graph.dot b/dependency-graph.dot index 254698f..a3cdc90 100644 --- a/dependency-graph.dot +++ b/dependency-graph.dot @@ -10,38 +10,65 @@ strict digraph "dependency-cruiser output"{ subgraph "cluster_fs" {label="fs" "fs/promises" [label= tooltip="promises" URL="https://nodejs.org/api/fs.html" color="grey" fontcolor="grey"] } "package.json" [label= tooltip="package.json" URL="package.json" fillcolor="#ffee44"] "path" [label= tooltip="path" URL="https://nodejs.org/api/path.html" color="grey" fontcolor="grey"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/config.ts" [label= tooltip="config.ts" URL="src/core/database/config.ts" fillcolor="#ddfeff"] } } } + "src/core/database/config.ts" -> "src/core/database/database.ts" + "src/core/database/config.ts" -> "src/core/database/helper.ts" [arrowhead="normalnoneodot"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/containerStats.ts" [label= tooltip="containerStats.ts" URL="src/core/database/containerStats.ts" fillcolor="#ddfeff"] } } } + "src/core/database/containerStats.ts" -> "src/core/database/database.ts" + "src/core/database/containerStats.ts" -> "src/core/database/helper.ts" [arrowhead="normalnoneodot"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/database.ts" [label= tooltip="database.ts" URL="src/core/database/database.ts" fillcolor="#ddfeff"] } } } + "src/core/database/database.ts" -> "bun:sqlite" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/dockerHosts.ts" [label= tooltip="dockerHosts.ts" URL="src/core/database/dockerHosts.ts" fillcolor="#ddfeff"] } } } + "src/core/database/dockerHosts.ts" -> "src/core/database/database.ts" + "src/core/database/dockerHosts.ts" -> "src/core/database/helper.ts" [arrowhead="normalnoneodot"] + "src/core/database/dockerHosts.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/helper.ts" [label= tooltip="helper.ts" URL="src/core/database/helper.ts" fillcolor="#ddfeff"] } } } "src/core/database/helper.ts" -> "src/core/utils/logger.ts" [arrowhead="normalnoneodot"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/repository.ts" [label= tooltip="repository.ts" URL="src/core/database/repository.ts" fillcolor="#ddfeff"] } } } - "src/core/database/repository.ts" -> "src/core/database/helper.ts" [arrowhead="normalnoneodot"] - "src/core/database/repository.ts" -> "src/core/utils/logger.ts" [arrowhead="normalnoneodot"] - "src/core/database/repository.ts" -> "src/typings/database.ts" [arrowhead="onormal" penwidth="1.0"] - "src/core/database/repository.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] - "src/core/database/repository.ts" -> "bun:sqlite" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/hostStats.ts" [label= tooltip="hostStats.ts" URL="src/core/database/hostStats.ts" fillcolor="#ddfeff"] } } } + "src/core/database/hostStats.ts" -> "src/core/database/database.ts" + "src/core/database/hostStats.ts" -> "src/core/database/helper.ts" [arrowhead="normalnoneodot"] + "src/core/database/hostStats.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/index.ts" [label= tooltip="index.ts" URL="src/core/database/index.ts" fillcolor="#ddfeff"] } } } + "src/core/database/index.ts" -> "src/core/database/config.ts" [arrowhead="normalnoneodot"] + "src/core/database/index.ts" -> "src/core/database/containerStats.ts" [arrowhead="normalnoneodot"] + "src/core/database/index.ts" -> "src/core/database/database.ts" + "src/core/database/index.ts" -> "src/core/database/dockerHosts.ts" [arrowhead="normalnoneodot"] + "src/core/database/index.ts" -> "src/core/database/hostStats.ts" [arrowhead="normalnoneodot"] + "src/core/database/index.ts" -> "src/core/database/logs.ts" [arrowhead="normalnoneodot"] + "src/core/database/index.ts" -> "src/core/database/stacks.ts" [arrowhead="normalnoneodot"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/logs.ts" [label= tooltip="logs.ts" URL="src/core/database/logs.ts" fillcolor="#ddfeff"] } } } + "src/core/database/logs.ts" -> "src/core/database/database.ts" + "src/core/database/logs.ts" -> "src/core/database/helper.ts" [arrowhead="normalnoneodot"] + "src/core/database/logs.ts" -> "src/typings/websocket.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/stacks.ts" [label= tooltip="stacks.ts" URL="src/core/database/stacks.ts" fillcolor="#ddfeff"] } } } + "src/core/database/stacks.ts" -> "src/core/database/database.ts" + "src/core/database/stacks.ts" -> "src/core/database/helper.ts" [arrowhead="normalnoneodot"] + "src/core/database/stacks.ts" -> "src/typings/database.ts" [arrowhead="onormal" penwidth="1.0"] + "src/core/database/stacks.ts" -> "src/typings/docker-compose.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/client.ts" [label= tooltip="client.ts" URL="src/core/docker/client.ts" fillcolor="#ddfeff"] } } } "src/core/docker/client.ts" -> "src/core/utils/logger.ts" "src/core/docker/client.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/monitor.ts" [label= tooltip="monitor.ts" URL="src/core/docker/monitor.ts" fillcolor="#ddfeff"] } } } "src/core/docker/monitor.ts" -> "src/core/plugins/plugin-manager.ts" - "src/core/docker/monitor.ts" -> "src/core/database/repository.ts" + "src/core/docker/monitor.ts" -> "src/core/database/index.ts" "src/core/docker/monitor.ts" -> "src/core/docker/client.ts" "src/core/docker/monitor.ts" -> "src/core/utils/logger.ts" "src/core/docker/monitor.ts" -> "src/typings/docker.ts" "src/core/docker/monitor.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] "src/core/docker/monitor.ts" -> "bun" subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/scheduler.ts" [label= tooltip="scheduler.ts" URL="src/core/docker/scheduler.ts" fillcolor="#ddfeff"] } } } - "src/core/docker/scheduler.ts" -> "src/core/database/repository.ts" + "src/core/docker/scheduler.ts" -> "src/core/database/index.ts" "src/core/docker/scheduler.ts" -> "src/core/docker/store-host-stats.ts" "src/core/docker/scheduler.ts" -> "src/core/docker/store-container-stats.ts" "src/core/docker/scheduler.ts" -> "src/core/utils/logger.ts" "src/core/docker/scheduler.ts" -> "src/typings/database.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/store-container-stats.ts" [label= tooltip="store-container-stats.ts" URL="src/core/docker/store-container-stats.ts" fillcolor="#ddfeff"] } } } "src/core/docker/store-container-stats.ts" -> "src/core/utils/logger.ts" - "src/core/docker/store-container-stats.ts" -> "src/core/database/repository.ts" + "src/core/docker/store-container-stats.ts" -> "src/core/database/index.ts" "src/core/docker/store-container-stats.ts" -> "src/core/docker/client.ts" "src/core/docker/store-container-stats.ts" -> "src/core/utils/calculations.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/store-host-stats.ts" [label= tooltip="store-host-stats.ts" URL="src/core/docker/store-host-stats.ts" fillcolor="#ddfeff"] } } } - "src/core/docker/store-host-stats.ts" -> "src/core/database/repository.ts" + "src/core/docker/store-host-stats.ts" -> "src/core/database/index.ts" "src/core/docker/store-host-stats.ts" -> "src/core/docker/client.ts" "src/core/docker/store-host-stats.ts" -> "src/core/utils/logger.ts" "src/core/docker/store-host-stats.ts" -> "src/typings/docker.ts" @@ -58,26 +85,28 @@ strict digraph "dependency-cruiser output"{ "src/core/plugins/plugin-manager.ts" -> "src/typings/plugin.ts" [arrowhead="onormal" penwidth="1.0"] "src/core/plugins/plugin-manager.ts" -> "events" [style="dashed" penwidth="1.0"] subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/stacks" {label="stacks" "src/core/stacks/controller.ts" [label= tooltip="controller.ts" URL="src/core/stacks/controller.ts" fillcolor="#ddfeff"] } } } - "src/core/stacks/controller.ts" -> "src/core/database/repository.ts" + "src/core/stacks/controller.ts" -> "src/core/database/index.ts" "src/core/stacks/controller.ts" -> "src/core/utils/logger.ts" "src/core/stacks/controller.ts" -> "src/typings/database.ts" [arrowhead="onormal" penwidth="1.0"] "src/core/stacks/controller.ts" -> "src/typings/docker-compose.ts" [arrowhead="onormal" penwidth="1.0"] + "src/core/stacks/controller.ts" -> "bun" + "src/core/stacks/controller.ts" -> "fs/promises" [style="dashed" penwidth="1.0"] subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" "src/core/trpc/index.ts" [label= tooltip="index.ts" URL="src/core/trpc/index.ts" fillcolor="#ddfeff"] } } } "src/core/trpc/index.ts" -> "src/core/trpc/router.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/api-config.procedure.ts" [label= tooltip="api-config.procedure.ts" URL="src/core/trpc/procedures/api-config.procedure.ts" fillcolor="#ddfeff"] } } } } "src/core/trpc/procedures/api-config.procedure.ts" -> "src/core/trpc/trpc.ts" - "src/core/trpc/procedures/api-config.procedure.ts" -> "src/core/database/repository.ts" + "src/core/trpc/procedures/api-config.procedure.ts" -> "src/core/database/index.ts" "src/core/trpc/procedures/api-config.procedure.ts" -> "src/core/utils/logger.ts" "src/core/trpc/procedures/api-config.procedure.ts" -> "src/core/utils/package-json.ts" "src/core/trpc/procedures/api-config.procedure.ts" -> "src/typings/database.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/docker-manager.procedure.ts" [label= tooltip="docker-manager.procedure.ts" URL="src/core/trpc/procedures/docker-manager.procedure.ts" fillcolor="#ddfeff"] } } } } "src/core/trpc/procedures/docker-manager.procedure.ts" -> "src/core/trpc/trpc.ts" - "src/core/trpc/procedures/docker-manager.procedure.ts" -> "src/core/database/repository.ts" + "src/core/trpc/procedures/docker-manager.procedure.ts" -> "src/core/database/index.ts" "src/core/trpc/procedures/docker-manager.procedure.ts" -> "src/core/utils/logger.ts" "src/core/trpc/procedures/docker-manager.procedure.ts" -> "src/typings/docker.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/docker-stats.procedure.ts" [label= tooltip="docker-stats.procedure.ts" URL="src/core/trpc/procedures/docker-stats.procedure.ts" fillcolor="#ddfeff"] } } } } "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/trpc/trpc.ts" - "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/database/repository.ts" + "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/database/index.ts" "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/docker/client.ts" "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/utils/calculations.ts" "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/utils/logger.ts" @@ -85,11 +114,11 @@ strict digraph "dependency-cruiser output"{ "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/typings/dockerode.ts" [arrowhead="onormal" penwidth="1.0"] subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/logs.procedure.ts" [label= tooltip="logs.procedure.ts" URL="src/core/trpc/procedures/logs.procedure.ts" fillcolor="#ddfeff"] } } } } "src/core/trpc/procedures/logs.procedure.ts" -> "src/core/trpc/trpc.ts" - "src/core/trpc/procedures/logs.procedure.ts" -> "src/core/database/repository.ts" + "src/core/trpc/procedures/logs.procedure.ts" -> "src/core/database/index.ts" "src/core/trpc/procedures/logs.procedure.ts" -> "src/core/utils/logger.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/stacks.procedure.ts" [label= tooltip="stacks.procedure.ts" URL="src/core/trpc/procedures/stacks.procedure.ts" fillcolor="#ddfeff"] } } } } "src/core/trpc/procedures/stacks.procedure.ts" -> "src/core/trpc/trpc.ts" - "src/core/trpc/procedures/stacks.procedure.ts" -> "src/core/database/repository.ts" + "src/core/trpc/procedures/stacks.procedure.ts" -> "src/core/database/index.ts" "src/core/trpc/procedures/stacks.procedure.ts" -> "src/core/stacks/controller.ts" "src/core/trpc/procedures/stacks.procedure.ts" -> "src/core/utils/logger.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" "src/core/trpc/router.ts" [label= tooltip="router.ts" URL="src/core/trpc/router.ts" fillcolor="#ddfeff"] } } } @@ -105,23 +134,25 @@ strict digraph "dependency-cruiser output"{ "src/core/utils/change-me-checker.ts" -> "src/core/utils/logger.ts" "src/core/utils/change-me-checker.ts" -> "fs/promises" [style="dashed" penwidth="1.0"] subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/logger.ts" [label= tooltip="logger.ts" URL="src/core/utils/logger.ts" fillcolor="#ddfeff"] } } } - "src/core/utils/logger.ts" -> "src/core/database/repository.ts" [arrowhead="normalnoneodot"] + "src/core/utils/logger.ts" -> "src/core/database/index.ts" [arrowhead="normalnoneodot"] "src/core/utils/logger.ts" -> "src/routes/live-logs.ts" [arrowhead="normalnoneodot"] - "src/core/utils/logger.ts" -> "src/typings/websocket.ts" + "src/core/utils/logger.ts" -> "src/typings/websocket.ts" [arrowhead="onormal" penwidth="1.0"] "src/core/utils/logger.ts" -> "path" [style="dashed" penwidth="1.0"] subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/package-json.ts" [label= tooltip="package-json.ts" URL="src/core/utils/package-json.ts" fillcolor="#ddfeff"] } } } "src/core/utils/package-json.ts" -> "package.json" subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/response-handler.ts" [label= tooltip="response-handler.ts" URL="src/core/utils/response-handler.ts" fillcolor="#ddfeff"] } } } "src/core/utils/response-handler.ts" -> "src/core/utils/logger.ts" "src/core/utils/response-handler.ts" -> "src/typings/elysiajs.ts" [arrowhead="onormal" penwidth="1.0"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/swagger-readme.ts" [label= tooltip="swagger-readme.ts" URL="src/core/utils/swagger-readme.ts" fillcolor="#ddfeff"] } } } subgraph "cluster_src" {label="src" "src/index.ts" [label= tooltip="index.ts" URL="src/index.ts" fillcolor="#ddfeff"] } "src/index.ts" -> "src/core/docker/monitor.ts" + "src/index.ts" -> "src/core/utils/swagger-readme.ts" "src/index.ts" -> "src/middleware/auth.ts" "src/index.ts" -> "src/routes/live-logs.ts" "src/index.ts" -> "src/routes/stacks.ts" "src/index.ts" -> "src/routes/utils.ts" "src/index.ts" -> "src/typings/database.ts" - "src/index.ts" -> "src/core/database/repository.ts" + "src/index.ts" -> "src/core/database/index.ts" "src/index.ts" -> "src/core/docker/scheduler.ts" "src/index.ts" -> "src/core/plugins/loader.ts" "src/index.ts" -> "src/core/trpc/index.ts" @@ -132,12 +163,12 @@ strict digraph "dependency-cruiser output"{ "src/index.ts" -> "src/routes/docker-websocket.ts" "src/index.ts" -> "src/routes/logs.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/middleware" {label="middleware" "src/middleware/auth.ts" [label= tooltip="auth.ts" URL="src/middleware/auth.ts" fillcolor="#ddfeff"] } } - "src/middleware/auth.ts" -> "src/core/database/repository.ts" + "src/middleware/auth.ts" -> "src/core/database/index.ts" "src/middleware/auth.ts" -> "src/core/utils/logger.ts" "src/middleware/auth.ts" -> "src/typings/database.ts" "src/middleware/auth.ts" -> "src/typings/elysiajs.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/api-config.ts" [label= tooltip="api-config.ts" URL="src/routes/api-config.ts" fillcolor="#ddfeff"] } } - "src/routes/api-config.ts" -> "src/core/database/repository.ts" + "src/routes/api-config.ts" -> "src/core/database/index.ts" "src/routes/api-config.ts" -> "src/core/plugins/plugin-manager.ts" "src/routes/api-config.ts" -> "src/core/utils/logger.ts" "src/routes/api-config.ts" -> "src/core/utils/package-json.ts" @@ -145,11 +176,12 @@ strict digraph "dependency-cruiser output"{ "src/routes/api-config.ts" -> "src/middleware/auth.ts" "src/routes/api-config.ts" -> "src/typings/database.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/docker-manager.ts" [label= tooltip="docker-manager.ts" URL="src/routes/docker-manager.ts" fillcolor="#ddfeff"] } } - "src/routes/docker-manager.ts" -> "src/core/database/repository.ts" + "src/routes/docker-manager.ts" -> "src/core/database/index.ts" "src/routes/docker-manager.ts" -> "src/core/utils/logger.ts" "src/routes/docker-manager.ts" -> "src/core/utils/response-handler.ts" + "src/routes/docker-manager.ts" -> "src/typings/docker.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/docker-stats.ts" [label= tooltip="docker-stats.ts" URL="src/routes/docker-stats.ts" fillcolor="#ddfeff"] } } - "src/routes/docker-stats.ts" -> "src/core/database/repository.ts" + "src/routes/docker-stats.ts" -> "src/core/database/index.ts" "src/routes/docker-stats.ts" -> "src/core/docker/client.ts" "src/routes/docker-stats.ts" -> "src/core/utils/calculations.ts" "src/routes/docker-stats.ts" -> "src/core/utils/logger.ts" @@ -157,7 +189,7 @@ strict digraph "dependency-cruiser output"{ "src/routes/docker-stats.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] "src/routes/docker-stats.ts" -> "src/typings/dockerode.ts" [arrowhead="onormal" penwidth="1.0"] subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/docker-websocket.ts" [label= tooltip="docker-websocket.ts" URL="src/routes/docker-websocket.ts" fillcolor="#ddfeff"] } } - "src/routes/docker-websocket.ts" -> "src/core/database/repository.ts" + "src/routes/docker-websocket.ts" -> "src/core/database/index.ts" "src/routes/docker-websocket.ts" -> "src/core/docker/client.ts" "src/routes/docker-websocket.ts" -> "src/core/utils/calculations.ts" "src/routes/docker-websocket.ts" -> "src/core/utils/logger.ts" @@ -167,10 +199,10 @@ strict digraph "dependency-cruiser output"{ "src/routes/live-logs.ts" -> "src/core/utils/logger.ts" [arrowhead="normalnoneodot"] "src/routes/live-logs.ts" -> "src/typings/websocket.ts" [arrowhead="onormal" penwidth="1.0"] subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/logs.ts" [label= tooltip="logs.ts" URL="src/routes/logs.ts" fillcolor="#ddfeff"] } } - "src/routes/logs.ts" -> "src/core/database/repository.ts" + "src/routes/logs.ts" -> "src/core/database/index.ts" "src/routes/logs.ts" -> "src/core/utils/logger.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/stacks.ts" [label= tooltip="stacks.ts" URL="src/routes/stacks.ts" fillcolor="#ddfeff"] } } - "src/routes/stacks.ts" -> "src/core/database/repository.ts" + "src/routes/stacks.ts" -> "src/core/database/index.ts" "src/routes/stacks.ts" -> "src/core/stacks/controller.ts" "src/routes/stacks.ts" -> "src/core/utils/logger.ts" "src/routes/stacks.ts" -> "src/core/utils/response-handler.ts" diff --git a/dependency-graph.mmd b/dependency-graph.mmd index ad13d50..bbe9dd9 100644 --- a/dependency-graph.mmd +++ b/dependency-graph.mmd @@ -11,207 +11,239 @@ subgraph 0["src"] subgraph 2["core"] subgraph 3["docker"] 4["monitor.ts"] -N["client.ts"] -Z["scheduler.ts"] -10["store-host-stats.ts"] -12["store-container-stats.ts"] +V["client.ts"] +19["scheduler.ts"] +1A["store-host-stats.ts"] +1C["store-container-stats.ts"] end subgraph 6["plugins"] 7["plugin-manager.ts"] -14["loader.ts"] +1E["loader.ts"] end subgraph 9["utils"] A["logger.ts"] -V["response-handler.ts"] -X["package-json.ts"] -13["calculations.ts"] -16["change-me-checker.ts"] +W["swagger-readme.ts"] +15["response-handler.ts"] +17["package-json.ts"] +1D["calculations.ts"] +1F["change-me-checker.ts"] end subgraph C["database"] -D["repository.ts"] -F["helper.ts"] +D["index.ts"] +E["config.ts"] +F["database.ts"] +H["helper.ts"] +I["containerStats.ts"] +J["dockerHosts.ts"] +M["hostStats.ts"] +N["logs.ts"] +P["stacks.ts"] end -subgraph S["stacks"] -T["controller.ts"] +subgraph 11["stacks"] +12["controller.ts"] end -subgraph 18["trpc"] -19["index.ts"] -1A["router.ts"] -subgraph 1B["procedures"] -1C["api-config.procedure.ts"] -1E["docker-manager.procedure.ts"] -1F["docker-stats.procedure.ts"] -1G["logs.procedure.ts"] -1H["stacks.procedure.ts"] +subgraph 1G["trpc"] +1H["index.ts"] +1I["router.ts"] +subgraph 1J["procedures"] +1K["api-config.procedure.ts"] +1M["docker-manager.procedure.ts"] +1N["docker-stats.procedure.ts"] +1O["logs.procedure.ts"] +1P["stacks.procedure.ts"] end -1D["trpc.ts"] +1L["trpc.ts"] end end -subgraph G["typings"] -H["database.ts"] -I["docker.ts"] -L["websocket.ts"] -M["plugin.ts"] -Q["elysiajs.ts"] -U["docker-compose.ts"] -11["dockerode.ts"] +subgraph K["typings"] +L["docker.ts"] +O["websocket.ts"] +Q["database.ts"] +R["docker-compose.ts"] +U["plugin.ts"] +Z["elysiajs.ts"] +1B["dockerode.ts"] end -subgraph J["routes"] -K["live-logs.ts"] -R["stacks.ts"] -W["utils.ts"] -1I["api-config.ts"] -1J["docker-manager.ts"] -1K["docker-stats.ts"] -1L["docker-websocket.ts"] -1N["logs.ts"] +subgraph S["routes"] +T["live-logs.ts"] +10["stacks.ts"] +16["utils.ts"] +1Q["api-config.ts"] +1R["docker-manager.ts"] +1S["docker-stats.ts"] +1T["docker-websocket.ts"] +1V["logs.ts"] end -subgraph O["middleware"] -P["auth.ts"] +subgraph X["middleware"] +Y["auth.ts"] end end 5["bun"] 8["events"] B["path"] -E["bun:sqlite"] -Y["package.json"] -subgraph 15["fs"] -17["promises"] +G["bun:sqlite"] +subgraph 13["fs"] +14["promises"] end -1M["stream"] +18["package.json"] +1U["stream"] 1-->4 -1-->P -1-->K -1-->R 1-->W -1-->H +1-->Y +1-->T +1-->10 +1-->16 +1-->Q 1-->D -1-->Z -1-->14 1-->19 +1-->1E +1-->1H 1-->A -1-->1I -1-->1J -1-->1K -1-->1L -1-->1N +1-->1Q +1-->1R +1-->1S +1-->1T +1-->1V 4-->7 4-->D -4-->N +4-->V 4-->A -4-->I -4-->I +4-->L +4-->L 4-->5 7-->A -7-->I -7-->M +7-->L +7-->U 7-->8 A-->D -A-->K -A-->L +A-->T +A-->O A-->B -D-->F -D-->A -D-->H -D-->I D-->E -F-->A -K-->A -K-->L -M-->I -N-->A -N-->I -P-->D -P-->A +D-->I +D-->F +D-->J +D-->M +D-->N +D-->P +E-->F +E-->H +F-->G +H-->A +I-->F +I-->H +J-->F +J-->H +J-->L +M-->F +M-->H +M-->L +N-->F +N-->H +N-->O +P-->F P-->H P-->Q -R-->D -R-->T -R-->A -R-->V -T-->D +P-->R T-->A -T-->H -T-->U +T-->O +U-->L V-->A -V-->Q -W-->X -W-->V -X-->Y -Z-->D -Z-->10 -Z-->12 -Z-->A -Z-->H +V-->L +Y-->D +Y-->A +Y-->Q +Y-->Z 10-->D -10-->N +10-->12 10-->A -10-->I -10-->11 -12-->A +10-->15 12-->D -12-->N -12-->13 -14-->16 -14-->A -14-->7 -14-->15 -14-->B -16-->A +12-->A +12-->Q +12-->R +12-->5 +12-->14 +15-->A +15-->Z 16-->17 +16-->15 +17-->18 +19-->D 19-->1A -1A-->1C -1A-->1E -1A-->1F -1A-->1G -1A-->1H -1A-->1D -1C-->1D -1C-->D +19-->1C +19-->A +19-->Q +1A-->D +1A-->V +1A-->A +1A-->L +1A-->1B 1C-->A -1C-->X -1C-->H -1E-->1D -1E-->D +1C-->D +1C-->V +1C-->1D +1E-->1F 1E-->A -1E-->I -1F-->1D -1F-->D -1F-->N -1F-->13 +1E-->7 +1E-->13 +1E-->B 1F-->A -1F-->I -1F-->11 -1G-->1D -1G-->D -1G-->A -1H-->1D -1H-->D -1H-->T -1H-->A -1I-->D -1I-->7 -1I-->A -1I-->X -1I-->V -1I-->P -1I-->H -1J-->D -1J-->A -1J-->V +1F-->14 +1H-->1I +1I-->1K +1I-->1M +1I-->1N +1I-->1O +1I-->1P +1I-->1L +1K-->1L 1K-->D -1K-->N -1K-->13 1K-->A -1K-->V -1K-->I -1K-->11 -1L-->D -1L-->N -1L-->13 -1L-->A -1L-->V -1L-->1M +1K-->17 +1K-->Q +1M-->1L +1M-->D +1M-->A +1M-->L +1N-->1L 1N-->D +1N-->V +1N-->1D 1N-->A +1N-->L +1N-->1B +1O-->1L +1O-->D +1O-->A +1P-->1L +1P-->D +1P-->12 +1P-->A +1Q-->D +1Q-->7 +1Q-->A +1Q-->17 +1Q-->15 +1Q-->Y +1Q-->Q +1R-->D +1R-->A +1R-->15 +1R-->L +1S-->D +1S-->V +1S-->1D +1S-->A +1S-->15 +1S-->L +1S-->1B +1T-->D +1T-->V +1T-->1D +1T-->A +1T-->15 +1T-->1U +1V-->D +1V-->A diff --git a/dependency-graph.svg b/dependency-graph.svg index 715f66a..c1e4807 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -4,82 +4,82 @@ - - + + dependency-cruiser output - + cluster_fs - -fs + +fs cluster_src - -src + +src cluster_src/core - -core + +core cluster_src/core/database - -database + +database cluster_src/core/docker - -docker + +docker cluster_src/core/plugins - -plugins + +plugins cluster_src/core/stacks - -stacks + +stacks cluster_src/core/trpc - -trpc + +trpc cluster_src/core/trpc/procedures - -procedures + +procedures cluster_src/core/utils - -utils + +utils cluster_src/middleware - -middleware + +middleware cluster_src/routes - -routes + +routes cluster_src/typings - -typings + +typings bun - -bun + +bun @@ -87,8 +87,8 @@ bun:sqlite - -bun:sqlite + +bun:sqlite @@ -96,8 +96,8 @@ events - -events + +events @@ -105,8 +105,8 @@ fs - -fs + +fs @@ -114,8 +114,8 @@ fs/promises - -promises + +promises @@ -123,8 +123,8 @@ package.json - -package.json + +package.json @@ -132,1195 +132,1431 @@ path - -path + +path - + +src/core/database/config.ts + + +config.ts + + + + + +src/core/database/database.ts + + +database.ts + + + + + +src/core/database/config.ts->src/core/database/database.ts + + + + + src/core/database/helper.ts - - -helper.ts + + +helper.ts + + +src/core/database/config.ts->src/core/database/helper.ts + + + + + + + +src/core/database/database.ts->bun:sqlite + + + - + src/core/utils/logger.ts - - -logger.ts + + +logger.ts - + src/core/database/helper.ts->src/core/utils/logger.ts - - - - + + + + + + + +src/core/database/containerStats.ts + + +containerStats.ts + + + + + +src/core/database/containerStats.ts->src/core/database/database.ts + + + + + +src/core/database/containerStats.ts->src/core/database/helper.ts + + + + + + + +src/core/database/dockerHosts.ts + + +dockerHosts.ts + + + + + +src/core/database/dockerHosts.ts->src/core/database/database.ts + + + + + +src/core/database/dockerHosts.ts->src/core/database/helper.ts + + + + + + + +src/typings/docker.ts + + +docker.ts + + + + + +src/core/database/dockerHosts.ts->src/typings/docker.ts + + - + src/core/utils/logger.ts->path - - + + - - -src/core/database/repository.ts - - -repository.ts + + +src/core/database/index.ts + + +index.ts - - -src/core/utils/logger.ts->src/core/database/repository.ts - - - - + + +src/core/utils/logger.ts->src/core/database/index.ts + + + + + + + +src/typings/websocket.ts + + +websocket.ts + + + + + +src/core/utils/logger.ts->src/typings/websocket.ts + + - + src/routes/live-logs.ts - - -live-logs.ts + + +live-logs.ts - + src/core/utils/logger.ts->src/routes/live-logs.ts - - - - + + + + - - -src/typings/websocket.ts - - -websocket.ts + + +src/core/database/hostStats.ts + + +hostStats.ts - - -src/core/utils/logger.ts->src/typings/websocket.ts - - + + +src/core/database/hostStats.ts->src/core/database/database.ts + + - - -src/core/database/repository.ts->bun:sqlite - - + + +src/core/database/hostStats.ts->src/core/database/helper.ts + + + + - - -src/core/database/repository.ts->src/core/database/helper.ts - - - - + + +src/core/database/hostStats.ts->src/typings/docker.ts + + - - -src/core/database/repository.ts->src/core/utils/logger.ts - - - - + + +src/core/database/index.ts->src/core/database/config.ts + + + + + + + +src/core/database/index.ts->src/core/database/database.ts + + + + + +src/core/database/index.ts->src/core/database/containerStats.ts + + + + + + + +src/core/database/index.ts->src/core/database/dockerHosts.ts + + + + + + + +src/core/database/index.ts->src/core/database/hostStats.ts + + + + + + + +src/core/database/logs.ts + + +logs.ts + + + + + +src/core/database/index.ts->src/core/database/logs.ts + + + + + + + +src/core/database/stacks.ts + + +stacks.ts + + + + + +src/core/database/index.ts->src/core/database/stacks.ts + + + + + + + +src/core/database/logs.ts->src/core/database/database.ts + + + + + +src/core/database/logs.ts->src/core/database/helper.ts + + + + + + + +src/core/database/logs.ts->src/typings/websocket.ts + + + + + +src/core/database/stacks.ts->src/core/database/database.ts + + + + + +src/core/database/stacks.ts->src/core/database/helper.ts + + + + - + src/typings/database.ts - - -database.ts + + +database.ts - - -src/core/database/repository.ts->src/typings/database.ts - - + + +src/core/database/stacks.ts->src/typings/database.ts + + - - -src/typings/docker.ts - - -docker.ts + + +src/typings/docker-compose.ts + + +docker-compose.ts - - -src/core/database/repository.ts->src/typings/docker.ts - - + + +src/core/database/stacks.ts->src/typings/docker-compose.ts + + - + src/core/docker/client.ts - - -client.ts + + +client.ts - - -src/core/docker/client.ts->src/core/utils/logger.ts - - - - + src/core/docker/client.ts->src/typings/docker.ts - - + + + + + +src/core/docker/client.ts->src/core/utils/logger.ts + + - + src/core/docker/monitor.ts - - -monitor.ts + + +monitor.ts - + src/core/docker/monitor.ts->bun - - + + + + + +src/core/docker/monitor.ts->src/typings/docker.ts + + - + src/core/docker/monitor.ts->src/core/utils/logger.ts - - + + - - -src/core/docker/monitor.ts->src/core/database/repository.ts - - - - - -src/core/docker/monitor.ts->src/typings/docker.ts - - + + +src/core/docker/monitor.ts->src/core/database/index.ts + + - + src/core/docker/monitor.ts->src/core/docker/client.ts - - + + - + src/core/plugins/plugin-manager.ts - - -plugin-manager.ts + + +plugin-manager.ts - + src/core/docker/monitor.ts->src/core/plugins/plugin-manager.ts - - + + - + src/core/plugins/plugin-manager.ts->events - - - - - -src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts - - + + - + src/core/plugins/plugin-manager.ts->src/typings/docker.ts - - + + + + + +src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts + + - + src/typings/plugin.ts - - -plugin.ts + + +plugin.ts - + src/core/plugins/plugin-manager.ts->src/typings/plugin.ts - - + + - + src/core/docker/scheduler.ts - - -scheduler.ts + + +scheduler.ts - + src/core/docker/scheduler.ts->src/core/utils/logger.ts - - + + - - -src/core/docker/scheduler.ts->src/core/database/repository.ts - - + + +src/core/docker/scheduler.ts->src/core/database/index.ts + + - + src/core/docker/scheduler.ts->src/typings/database.ts - - + + - + src/core/docker/store-host-stats.ts - - -store-host-stats.ts + + +store-host-stats.ts - + src/core/docker/scheduler.ts->src/core/docker/store-host-stats.ts - - + + - + src/core/docker/store-container-stats.ts - - -store-container-stats.ts + + +store-container-stats.ts - + src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts - - + + + + + +src/core/docker/store-host-stats.ts->src/typings/docker.ts + + - + src/core/docker/store-host-stats.ts->src/core/utils/logger.ts - - + + - - -src/core/docker/store-host-stats.ts->src/core/database/repository.ts - - - - - -src/core/docker/store-host-stats.ts->src/typings/docker.ts - - + + +src/core/docker/store-host-stats.ts->src/core/database/index.ts + + - + src/core/docker/store-host-stats.ts->src/core/docker/client.ts - - + + - + src/typings/dockerode.ts - - -dockerode.ts + + +dockerode.ts - + src/core/docker/store-host-stats.ts->src/typings/dockerode.ts - - + + - + src/core/docker/store-container-stats.ts->src/core/utils/logger.ts - - + + - - -src/core/docker/store-container-stats.ts->src/core/database/repository.ts - - + + +src/core/docker/store-container-stats.ts->src/core/database/index.ts + + - + src/core/docker/store-container-stats.ts->src/core/docker/client.ts - - + + - + src/core/utils/calculations.ts - - -calculations.ts + + +calculations.ts - + src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts - - + + - + src/core/plugins/loader.ts - - -loader.ts + + +loader.ts - + src/core/plugins/loader.ts->fs - - + + - + src/core/plugins/loader.ts->path - - + + - + src/core/plugins/loader.ts->src/core/utils/logger.ts - - + + - + src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts - - + + - + src/core/utils/change-me-checker.ts - - -change-me-checker.ts + + +change-me-checker.ts - + src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts - - + + - + src/core/utils/change-me-checker.ts->fs/promises - - + + - + src/core/utils/change-me-checker.ts->src/core/utils/logger.ts - - + + - + src/typings/plugin.ts->src/typings/docker.ts - - + + - + src/core/stacks/controller.ts - - -controller.ts + + +controller.ts + + +src/core/stacks/controller.ts->bun + + + + + +src/core/stacks/controller.ts->fs/promises + + + - + src/core/stacks/controller.ts->src/core/utils/logger.ts - - + + - - -src/core/stacks/controller.ts->src/core/database/repository.ts - - + + +src/core/stacks/controller.ts->src/core/database/index.ts + + - + src/core/stacks/controller.ts->src/typings/database.ts - - - - - -src/typings/docker-compose.ts - - -docker-compose.ts - - + + - + src/core/stacks/controller.ts->src/typings/docker-compose.ts - - + + - + src/core/trpc/index.ts - - -index.ts + + +index.ts - + src/core/trpc/router.ts - - -router.ts + + +router.ts - + src/core/trpc/index.ts->src/core/trpc/router.ts - - + + - + src/core/trpc/procedures/api-config.procedure.ts - - -api-config.procedure.ts + + +api-config.procedure.ts - + src/core/trpc/router.ts->src/core/trpc/procedures/api-config.procedure.ts - - + + - + src/core/trpc/trpc.ts - - -trpc.ts + + +trpc.ts - + src/core/trpc/router.ts->src/core/trpc/trpc.ts - - + + - + src/core/trpc/procedures/docker-manager.procedure.ts - - -docker-manager.procedure.ts + + +docker-manager.procedure.ts - + src/core/trpc/router.ts->src/core/trpc/procedures/docker-manager.procedure.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts - - -docker-stats.procedure.ts + + +docker-stats.procedure.ts - + src/core/trpc/router.ts->src/core/trpc/procedures/docker-stats.procedure.ts - - + + - + src/core/trpc/procedures/logs.procedure.ts - - -logs.procedure.ts + + +logs.procedure.ts - + src/core/trpc/router.ts->src/core/trpc/procedures/logs.procedure.ts - - + + - + src/core/trpc/procedures/stacks.procedure.ts - - -stacks.procedure.ts + + +stacks.procedure.ts - + src/core/trpc/router.ts->src/core/trpc/procedures/stacks.procedure.ts - - + + - + src/core/trpc/procedures/api-config.procedure.ts->src/core/utils/logger.ts - - + + - - -src/core/trpc/procedures/api-config.procedure.ts->src/core/database/repository.ts - - + + +src/core/trpc/procedures/api-config.procedure.ts->src/core/database/index.ts + + - + src/core/trpc/procedures/api-config.procedure.ts->src/typings/database.ts - - + + - + src/core/trpc/procedures/api-config.procedure.ts->src/core/trpc/trpc.ts - - + + - + src/core/utils/package-json.ts - - -package-json.ts + + +package-json.ts - + src/core/trpc/procedures/api-config.procedure.ts->src/core/utils/package-json.ts - - + + - + src/core/utils/package-json.ts->package.json - - + + + + + +src/core/trpc/procedures/docker-manager.procedure.ts->src/typings/docker.ts + + - + src/core/trpc/procedures/docker-manager.procedure.ts->src/core/utils/logger.ts - - - - - -src/core/trpc/procedures/docker-manager.procedure.ts->src/core/database/repository.ts - - + + - - -src/core/trpc/procedures/docker-manager.procedure.ts->src/typings/docker.ts - - + + +src/core/trpc/procedures/docker-manager.procedure.ts->src/core/database/index.ts + + - + src/core/trpc/procedures/docker-manager.procedure.ts->src/core/trpc/trpc.ts - - + + + + + +src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/docker.ts + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/utils/logger.ts - - - - - -src/core/trpc/procedures/docker-stats.procedure.ts->src/core/database/repository.ts - - + + - - -src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/docker.ts - - + + +src/core/trpc/procedures/docker-stats.procedure.ts->src/core/database/index.ts + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/docker/client.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/utils/calculations.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/dockerode.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/trpc/trpc.ts - - + + - + src/core/trpc/procedures/logs.procedure.ts->src/core/utils/logger.ts - - + + - - -src/core/trpc/procedures/logs.procedure.ts->src/core/database/repository.ts - - + + +src/core/trpc/procedures/logs.procedure.ts->src/core/database/index.ts + + - + src/core/trpc/procedures/logs.procedure.ts->src/core/trpc/trpc.ts - - + + - + src/core/trpc/procedures/stacks.procedure.ts->src/core/utils/logger.ts - - + + - - -src/core/trpc/procedures/stacks.procedure.ts->src/core/database/repository.ts - - + + +src/core/trpc/procedures/stacks.procedure.ts->src/core/database/index.ts + + - + src/core/trpc/procedures/stacks.procedure.ts->src/core/stacks/controller.ts - - + + - + src/core/trpc/procedures/stacks.procedure.ts->src/core/trpc/trpc.ts - - + + - + src/routes/live-logs.ts->src/core/utils/logger.ts - - - - + + + + - + src/routes/live-logs.ts->src/typings/websocket.ts - - + + - + src/core/utils/response-handler.ts - - -response-handler.ts + + +response-handler.ts - + src/core/utils/response-handler.ts->src/core/utils/logger.ts - - + + - + src/typings/elysiajs.ts - - -elysiajs.ts + + +elysiajs.ts - + src/core/utils/response-handler.ts->src/typings/elysiajs.ts - - + + + + + +src/core/utils/swagger-readme.ts + + +swagger-readme.ts + + - + src/index.ts - - -index.ts + + +index.ts - + src/index.ts->src/core/utils/logger.ts - - + + - - -src/index.ts->src/core/database/repository.ts - - + + +src/index.ts->src/core/database/index.ts + + - + src/index.ts->src/typings/database.ts - - + + - + src/index.ts->src/core/docker/monitor.ts - - + + - + src/index.ts->src/core/docker/scheduler.ts - - + + - + src/index.ts->src/core/plugins/loader.ts - - + + - + src/index.ts->src/core/trpc/index.ts - - + + - + src/index.ts->src/routes/live-logs.ts - - + + + + + +src/index.ts->src/core/utils/swagger-readme.ts + + - + src/middleware/auth.ts - - -auth.ts + + +auth.ts - + src/index.ts->src/middleware/auth.ts - - + + - + src/routes/stacks.ts - - -stacks.ts + + +stacks.ts - + src/index.ts->src/routes/stacks.ts - - + + - + src/routes/utils.ts - - -utils.ts + + +utils.ts - + src/index.ts->src/routes/utils.ts - - + + - + src/routes/api-config.ts - - -api-config.ts + + +api-config.ts - + src/index.ts->src/routes/api-config.ts - - + + - + src/routes/docker-manager.ts - - -docker-manager.ts + + +docker-manager.ts - + src/index.ts->src/routes/docker-manager.ts - - + + - + src/routes/docker-stats.ts - - -docker-stats.ts + + +docker-stats.ts - + src/index.ts->src/routes/docker-stats.ts - - + + - + src/routes/docker-websocket.ts - - -docker-websocket.ts + + +docker-websocket.ts - + src/index.ts->src/routes/docker-websocket.ts - - + + - + src/routes/logs.ts - - -logs.ts + + +logs.ts - + src/index.ts->src/routes/logs.ts - - + + - + src/middleware/auth.ts->src/core/utils/logger.ts - - + + - - -src/middleware/auth.ts->src/core/database/repository.ts - - + + +src/middleware/auth.ts->src/core/database/index.ts + + - + src/middleware/auth.ts->src/typings/database.ts - - + + - + src/middleware/auth.ts->src/typings/elysiajs.ts - - + + - + src/routes/stacks.ts->src/core/utils/logger.ts - - + + - - -src/routes/stacks.ts->src/core/database/repository.ts - - + + +src/routes/stacks.ts->src/core/database/index.ts + + - + src/routes/stacks.ts->src/core/stacks/controller.ts - - + + - + src/routes/stacks.ts->src/core/utils/response-handler.ts - - + + - + src/routes/utils.ts->src/core/utils/package-json.ts - - + + - + src/routes/utils.ts->src/core/utils/response-handler.ts - - + + - + src/routes/api-config.ts->src/core/utils/logger.ts - - + + - - -src/routes/api-config.ts->src/core/database/repository.ts - - + + +src/routes/api-config.ts->src/core/database/index.ts + + - + src/routes/api-config.ts->src/typings/database.ts - - + + - + src/routes/api-config.ts->src/core/plugins/plugin-manager.ts - - + + - + src/routes/api-config.ts->src/core/utils/package-json.ts - - + + - + src/routes/api-config.ts->src/core/utils/response-handler.ts - - + + - + src/routes/api-config.ts->src/middleware/auth.ts - - + + + + + +src/routes/docker-manager.ts->src/typings/docker.ts + + - + src/routes/docker-manager.ts->src/core/utils/logger.ts - - + + - - -src/routes/docker-manager.ts->src/core/database/repository.ts - - + + +src/routes/docker-manager.ts->src/core/database/index.ts + + - + src/routes/docker-manager.ts->src/core/utils/response-handler.ts - - + + + + + +src/routes/docker-stats.ts->src/typings/docker.ts + + - + src/routes/docker-stats.ts->src/core/utils/logger.ts - - - - - -src/routes/docker-stats.ts->src/core/database/repository.ts - - + + - - -src/routes/docker-stats.ts->src/typings/docker.ts - - + + +src/routes/docker-stats.ts->src/core/database/index.ts + + - + src/routes/docker-stats.ts->src/core/docker/client.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-stats.ts->src/typings/dockerode.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/response-handler.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/logger.ts - - + + - - -src/routes/docker-websocket.ts->src/core/database/repository.ts - - + + +src/routes/docker-websocket.ts->src/core/database/index.ts + + - + src/routes/docker-websocket.ts->src/core/docker/client.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/response-handler.ts - - + + - + stream - - -stream + + +stream - + src/routes/docker-websocket.ts->stream - - + + - + src/routes/logs.ts->src/core/utils/logger.ts - - - - - -src/routes/logs.ts->src/core/database/repository.ts - - + + + + + +src/routes/logs.ts->src/core/database/index.ts + + From 84125a96a3d39f71b081bd904bece3f031d7cea5 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Sun, 30 Mar 2025 00:01:18 +0100 Subject: [PATCH 229/324] Fix: Delete unused files --- src/core/database/repository.ts | 565 -------------------------------- 1 file changed, 565 deletions(-) delete mode 100644 src/core/database/repository.ts diff --git a/src/core/database/repository.ts b/src/core/database/repository.ts deleted file mode 100644 index 1c920a7..0000000 --- a/src/core/database/repository.ts +++ /dev/null @@ -1,565 +0,0 @@ -import { executeDbOperation } from "./helper"; -import Database from "bun:sqlite"; -import { logger } from "~/core/utils/logger"; -import type { DockerHost, HostStats } from "~/typings/docker"; -import type { config, stacks_config } from "~/typings/database"; - -const db = new Database("dockstatapi.db", { strict: true }); -db.exec("PRAGMA journal_mode = WAL;"); - -export const dbFunctions = { - init() { - const startTime = Date.now(); - db.exec(` - CREATE TABLE IF NOT EXISTS backend_log_entries ( - timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, - level TEXT NOT NULL, - message TEXT NOT NULL, - file TEXT NOT NULL, - line NUMBER NOT NULL - ); - - CREATE TABLE IF NOT EXISTS stacks_config ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - version INTEGER NOT NULL, - custom BOOLEAN NOT NULL, - source TEXT NOT NULL, - container_count INTEGER NOT NULL, - stack_prefix TEXT NOT NULL, - automatic_reboot_on_error BOOLEAN NOT NULL, - image_updates BOOLEAN NOT NULL - ); - - CREATE TABLE IF NOT EXISTS docker_hosts ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - hostAddress TEXT NOT NULL, - secure BOOLEAN NOT NULL - ); - - CREATE TABLE IF NOT EXISTS host_stats ( - hostId INTEGER PRIMARY KEY NOT NULL, - hostName TEXT NOT NULL, - dockerVersion TEXT NOT NULL, - apiVersion TEXT NOT NULL, - os TEXT NOT NULL, - architecture TEXT NOT NULL, - totalMemory INTEGER NOT NULL, - totalCPU INTEGER NOT NULL, - labels TEXT NOT NULL, - containers INTEGER NOT NULL, - containersRunning INTEGER NOT NULL, - containersStopped INTEGER NOT NULL, - containersPaused INTEGER NOT NULL, - images INTEGER NOT NULL - ); - - CREATE TABLE IF NOT EXISTS container_stats ( - id TEXT NOT NULL, - hostId TEXT NOT NULL, - name TEXT NOT NULL, - image TEXT NOT NULL, - status TEXT NOT NULL, - state TEXT NOT NULL, - cpu_usage FLOAT NOT NULL, - memory_usage FLOAT NOT NULL, - timestamp DATETIME DEFAULT CURRENT_TIMESTAMP - ); - - CREATE TABLE IF NOT EXISTS config ( - keep_data_for NUMBER NOT NULL, - fetching_interval NUMBER NOT NULL, - api_key TEXT NOT NULL - ); - `); - - logger.info("Starting server..."); - - /* - * Default values: - * - Data retention value for the database (logs and container stats) 7 days - * - Data fetcher for the Database: 5 minutes - * - api_key: changeme - */ - const configRow = db - .prepare(`SELECT COUNT(*) AS count FROM config`) - .get() as { count: number }; - if (configRow.count === 0) { - logger.debug("Initializing default config"); - const stmt = db.prepare( - ` - INSERT INTO config (keep_data_for, fetching_interval, api_key) VALUES (7, 5, "changeme") - `, - ); - stmt.run(); - } - - const hostRow = db - .prepare(`SELECT COUNT(*) AS count FROM docker_hosts`) - .get("Localhost") as { count: number }; - if (hostRow.count === 0) { - logger.debug("Initializing default docker host (Localhost)"); - const stmt = db.prepare( - ` - INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?) - `, - ); - stmt.run("Localhost", "localhost:2375", false); - } - logger.debug("__task__ __db__ Initializing Database ⏳"); - const duration = Date.now() - startTime; - logger.debug(`__task__ __db__ Initializing Database ✔️ (${duration}ms)`); - }, - - addDockerHost(host: DockerHost) { - return executeDbOperation( - "Add Docker Host", - () => { - const stmt = db.prepare(` - INSERT INTO docker_hosts (name, hostAddress, secure) - VALUES (?, ?, ?) - `); - return stmt.run(host.name, host.hostAddress, host.secure); - }, - () => { - if (host.name.length < 1) { - logger.error("Hostname needed"); - throw new Error("Invalid data provided - Hostname needed"); - } - - if (host.hostAddress.length < 1) { - logger.error("hostAddress needed"); - throw new Error("Invalid data provided - hostAddress needed"); - } - - if ( - typeof host.name !== "string" || - typeof host.secure !== "boolean" || - typeof host.hostAddress !== "string" - ) { - logger.error("Invalid parameter types for addDockerHost"); - throw new TypeError("Invalid parameter types for addDockerHost"); - } - }, - ); - }, - - getDockerHosts(): DockerHost[] { - return executeDbOperation( - "Get Docker Hosts", - () => { - const stmt = db.prepare(` - SELECT id, name, hostAddress, secure - FROM docker_hosts - ORDER BY id DESC - `); - return stmt.all() as DockerHost[]; - }, - () => {}, - ); - }, - - addLogEntry: ( - level: string, - message: string, - file_name: string, - line: number, - ) => { - if ( - typeof level !== "string" || - typeof message !== "string" || - typeof file_name !== "string" || - typeof line !== "number" - ) { - logger.crit("Invalid parameter types for addLogEntry"); - throw new TypeError("Invalid parameter types for addLogEntry"); - } - - const stmt = db.prepare(` - INSERT INTO backend_log_entries (level, message, file, line) - VALUES (?, ?, ?, ?) - `); - return stmt.run(level, message, file_name, line); - }, - - getAllLogs() { - return executeDbOperation( - "Get All Logs", - () => { - const stmt = db.prepare(` - SELECT timestamp, level, message, file, line - FROM backend_log_entries - ORDER BY timestamp DESC - `); - return stmt.all(); - }, - () => {}, - ); - }, - - getLogsByLevel(level: string) { - return executeDbOperation( - "Get Logs By Level", - () => { - const stmt = db.prepare(` - SELECT timestamp, level, message, file, line - FROM backend_log_entries - WHERE level = ? - ORDER BY timestamp DESC - `); - return stmt.all(level); - }, - () => { - if (typeof level !== "string") { - logger.error("Level parameter must be a string"); - throw new TypeError("Level parameter must be a string"); - } - }, - ); - }, - - updateDockerHost(host: DockerHost) { - return executeDbOperation( - "Update Docker Host", - () => { - const stmt = db.prepare(` - UPDATE docker_hosts - SET hostAddress = ?, secure = ?, name = ? - WHERE id = ? - `); - return stmt.run( - host.hostAddress, - host.secure, - host.name, - String(host.id), - ); - }, - () => { - if ( - typeof host.name !== "string" || - typeof host.hostAddress !== "string" || - typeof host.secure !== "boolean" || - typeof host.id !== "number" - ) { - logger.error("Invalid parameter types for updateDockerHost"); - throw new TypeError("Invalid parameter types for updateDockerHost"); - } - }, - ); - }, - - deleteDockerHost(id: number) { - return executeDbOperation( - "Delete Docker Host", - () => { - const stmt = db.prepare(` - DELETE FROM docker_hosts - WHERE id = ? - `); - return stmt.run(id); - }, - () => { - if (typeof id !== "number") { - logger.error("Invalid parameter type for deleteDockerHost"); - throw new TypeError("id parameter must be a number"); - } - }, - ); - }, - - clearAllLogs() { - return executeDbOperation( - "Clear All Logs", - () => { - const stmt = db.prepare(` - DELETE FROM backend_log_entries - `); - return stmt.run(); - }, - () => {}, - ); - }, - - clearLogsByLevel(level: string) { - return executeDbOperation( - "Clear Logs By Level", - () => { - const stmt = db.prepare(` - DELETE FROM backend_log_entries - WHERE level = ? - `); - return stmt.run(level); - }, - () => { - if (typeof level !== "string") { - logger.error("Invalid parameter type for clearLogsByLevel"); - throw new TypeError("Level parameter must be a string"); - } - }, - ); - }, - - updateConfig( - fetching_interval: number, - keep_data_for: number, - api_key: string, - ) { - return executeDbOperation( - "Update Config", - () => { - const stmt = db.prepare(` - UPDATE config - SET fetching_interval = ?, - keep_data_for = ?, - api_key = ? - `); - return stmt.run(fetching_interval, keep_data_for, api_key); - }, - () => { - if ( - typeof fetching_interval !== "number" || - typeof keep_data_for !== "number" - ) { - logger.error("Invalid parameter types for updateConfig"); - throw new TypeError("Invalid parameter types for updateConfig"); - } - }, - ); - }, - - getConfig() { - return executeDbOperation( - "Get Config", - () => { - const stmt = db.prepare(` - SELECT keep_data_for, fetching_interval, api_key - FROM config - `); - return stmt.all(); - }, - () => {}, - ); - }, - - deleteOldData(days: number) { - return executeDbOperation( - "Delete Old Data", - () => { - const deleteContainerStmt = db.prepare(` - DELETE FROM container_stats - WHERE timestamp < datetime('now', '-' || ? || ' days') - `); - deleteContainerStmt.run(days); - - const deleteLogsStmt = db.prepare(` - DELETE FROM backend_log_entries - WHERE timestamp < datetime('now', '-' || ? || ' days') - `); - deleteLogsStmt.run(days); - }, - () => { - if (typeof days !== "number") { - logger.error("Invalid parameter type for deleteOldData"); - throw new TypeError("Days parameter must be a number"); - } - }, - ); - }, - - addContainerStats( - id: string, - hostId: string, - name: string, - image: string, - status: string, - state: string, - cpu_usage: number, - memory_usage: number, - ) { - return executeDbOperation( - "Add Container Stats", - () => { - const stmt = db.prepare(` - INSERT INTO container_stats (id, hostId, name, image, status, state, cpu_usage, memory_usage) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - `); - return stmt.run( - id, - hostId, - name, - image, - status, - state, - cpu_usage, - memory_usage, - ); - }, - () => { - if ( - typeof id !== "string" || - typeof hostId !== "string" || - typeof name !== "string" || - typeof image !== "string" || - typeof status !== "string" || - typeof state !== "string" || - typeof cpu_usage !== "number" || - typeof memory_usage !== "number" - ) { - logger.error("Invalid parameter types for addContainerStats"); - throw new TypeError("Invalid parameter types for addContainerStats"); - } - }, - ); - }, - - updateHostStats(stats: HostStats) { - return executeDbOperation( - "Update Host Stats", - () => { - const labelsJson = JSON.stringify(stats.labels); - const stmt = db.prepare(` - INSERT INTO host_stats ( - hostId, - hostName, - dockerVersion, - apiVersion, - os, - architecture, - totalMemory, - totalCPU, - labels, - containers, - containersRunning, - containersStopped, - containersPaused, - images - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ON CONFLICT(hostId) DO UPDATE SET - dockerVersion = excluded.dockerVersion, - apiVersion = excluded.apiVersion, - os = excluded.os, - architecture = excluded.architecture, - totalMemory = excluded.totalMemory, - totalCPU = excluded.totalCPU, - labels = excluded.labels, - containers = excluded.containers, - containersRunning = excluded.containersRunning, - containersStopped = excluded.containersStopped, - containersPaused = excluded.containersPaused, - images = excluded.images; - `); - return stmt.run( - stats.hostId, - stats.hostName, - stats.dockerVersion, - stats.apiVersion, - stats.os, - stats.architecture, - stats.totalMemory, - stats.totalCPU, - labelsJson, - stats.containers, - stats.containersRunning, - stats.containersStopped, - stats.containersPaused, - stats.images, - ); - }, - () => {}, - ); - }, - - addStack(stack_config: stacks_config) { - return executeDbOperation( - "Add Stack Config", - () => { - const stmt = db.prepare(` - INSERT INTO stacks_config ( - name, - version, - custom, - source, - container_count, - stack_prefix, - automatic_reboot_on_error, - image_updates - ) - VALUES(?, ?, ?, ?, ?, ?, ?, ?) - `); - return stmt.run( - stack_config.name, - stack_config.version, - stack_config.custom, - stack_config.source, - stack_config.container_count, - stack_config.stack_prefix, - stack_config.automatic_reboot_on_error, - stack_config.image_updates, - ); - }, - () => {}, - ); - }, - - getStacks() { - return executeDbOperation( - "Get Stacks", - () => { - const stmt = db.prepare(` - SELECT name, version, custom, source, container_count, stack_prefix, automatic_reboot_on_error, image_updates - FROM stacks_config - ORDER BY name DESC - `); - return stmt.all(); - }, - () => {}, - ); - }, - - deleteStack(id: number) { - return executeDbOperation( - "Delete Stack", - () => { - const stmt = db.prepare(` - DELETE FROM stacks_config - WHERE id = ?; - `); - return stmt.run(id); - }, - () => {}, - ); - }, - - updateStack(stack_config: stacks_config) { - return executeDbOperation( - "Update Stack", - () => { - const stmt = db.prepare(` - UPDATE stacks_config - SET - version = ?, - custom = ?, - source = ?, - container_count = ?, - stack_prefix = ?, - automatic_reboot_on_error = ?, - image_updates = ? - WHERE name = ?; - `); - return stmt.run( - stack_config.version, - stack_config.custom, - stack_config.source, - stack_config.container_count, - stack_config.stack_prefix, - stack_config.automatic_reboot_on_error, - stack_config.image_updates, - stack_config.name, - ); - }, - () => {}, - ); - }, -}; From 15d7d0609c57935186ba0f050348464e0289a10d Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Sun, 30 Mar 2025 00:03:04 +0100 Subject: [PATCH 230/324] Fix: Remove unused file and update dependencies --- bun.lock | 21 ++++++++++++++++----- package.json | 9 +++++---- src/core/utils/helpers.ts | 15 --------------- 3 files changed, 21 insertions(+), 24 deletions(-) delete mode 100644 src/core/utils/helpers.ts diff --git a/bun.lock b/bun.lock index b8dfca8..afc9523 100644 --- a/bun.lock +++ b/bun.lock @@ -24,6 +24,7 @@ "@types/split2": "^4.2.3", "bun-types": "latest", "cross-env": "^7.0.3", + "logform": "^2.7.0", "typescript": "^5.8.2", "wrap-ansi": "^9.0.0", "zod": "^3.24.2", @@ -94,9 +95,9 @@ "@types/docker-modem": ["@types/docker-modem@3.0.6", "", { "dependencies": { "@types/node": "*", "@types/ssh2": "*" } }, "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg=="], - "@types/dockerode": ["@types/dockerode@3.3.35", "", { "dependencies": { "@types/docker-modem": "*", "@types/node": "*", "@types/ssh2": "*" } }, "sha512-P+DCMASlsH+QaKkDpekKrP5pLls767PPs+/LrlVbKnEnY5tMpEUa2C6U4gRsdFZengOqxdCIqy16R22Q3pLB6Q=="], + "@types/dockerode": ["@types/dockerode@3.3.36", "", { "dependencies": { "@types/docker-modem": "*", "@types/node": "*", "@types/ssh2": "*" } }, "sha512-K0wTBKjjVI1xS4zeLynssmmbpPl4AnWZ/MJ3JBTi9eGzEmu+xgMLVSKiWzsy/z+3GBPLD5+uE/i/6ZTeZPaX7A=="], - "@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="], + "@types/node": ["@types/node@22.13.14", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w=="], "@types/split2": ["@types/split2@4.2.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-59OXIlfUsi2k++H6CHgUQKEb2HKRokUA39HY1i1dS8/AIcqVjtAAFdf8u+HxTWK/4FUHMJQlKSZ4I6irCBJ1Zw=="], @@ -132,7 +133,7 @@ "buildcheck": ["buildcheck@0.0.6", "", {}, "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A=="], - "bun-types": ["bun-types@1.2.5", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-3oO6LVGGRRKI4kHINx5PIdIgnLRb7l/SprhzqXapmoYkFl5m4j6EvALvbDVuuBFaamB46Ap6HCUxIXNLCGy+tg=="], + "bun-types": ["bun-types@1.2.7", "", { "dependencies": { "@types/node": "*", "@types/ws": "*" } }, "sha512-P4hHhk7kjF99acXqKvltyuMQ2kf/rzIw3ylEDpCxDS9Xa0X0Yp/gJu/vDCucmWpiur5qJ0lwB2bWzOXa2GlHqA=="], "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], @@ -236,7 +237,7 @@ "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], - "knip": ["knip@5.46.0", "", { "dependencies": { "@nodelib/fs.walk": "3.0.1", "@snyk/github-codeowners": "1.1.0", "easy-table": "1.2.0", "enhanced-resolve": "^5.18.0", "fast-glob": "^3.3.3", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "minimist": "^1.2.8", "picocolors": "^1.1.0", "picomatch": "^4.0.1", "pretty-ms": "^9.0.0", "smol-toml": "^1.3.1", "strip-json-comments": "5.0.1", "summary": "2.1.0", "zod": "^3.22.4", "zod-validation-error": "^3.0.3" }, "peerDependencies": { "@types/node": ">=18", "typescript": ">=5.0.4" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-WedHSK5xNBWYgm64Rt5B9b0CVXL2kRBcyCeet3NHgdv9en3QE4AWSDPEiX48NoPUBW3h//9S0VwLF5MG/MPi3g=="], + "knip": ["knip@5.46.3", "", { "dependencies": { "@nodelib/fs.walk": "3.0.1", "@snyk/github-codeowners": "1.1.0", "easy-table": "1.2.0", "enhanced-resolve": "^5.18.0", "fast-glob": "^3.3.3", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "minimist": "^1.2.8", "picocolors": "^1.1.0", "picomatch": "^4.0.1", "pretty-ms": "^9.0.0", "smol-toml": "^1.3.1", "strip-json-comments": "5.0.1", "summary": "2.1.0", "zod": "^3.22.4", "zod-validation-error": "^3.0.3" }, "peerDependencies": { "@types/node": ">=18", "typescript": ">=5.0.4" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-DpxZYvFDh0POjgnfXie39zd4SCxmw3iQTSLPgnf1Umq+k+sCHjcv553UmI3hfo39qlVIq2c8XSsjS3IeZfdAoA=="], "kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="], @@ -364,7 +365,7 @@ "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], - "yaml": ["yaml@2.7.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA=="], + "yaml": ["yaml@2.7.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ=="], "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], @@ -380,8 +381,14 @@ "@scalar/themes/@scalar/types": ["@scalar/types@0.1.2", "", { "dependencies": { "@scalar/openapi-types": "0.1.9", "@unhead/schema": "^1.11.11", "zod": "^3.23.8" } }, "sha512-5kCLQRwAYWt1ds110EaUb9yonc3KoQYNyo4YUCigJLOnoNugbqkEX0zRudGevItiuk+xg4uOYd30r3C+6xAasA=="], + "@types/docker-modem/@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="], + + "@types/split2/@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="], + "@types/ssh2/@types/node": ["@types/node@18.19.80", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-kEWeMwMeIvxYkeg1gTc01awpwLbfMRZXdIhwRcakd/KlK53jmRC26LqcbIt7fnAQTu5GzlnWmzA3H6+l1u6xxQ=="], + "@types/ws/@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="], + "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -392,12 +399,16 @@ "defaults/clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], + "docker-compose/yaml": ["yaml@2.7.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA=="], + "easy-table/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "fast-glob/@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "protobufjs/@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="], + "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.1.9", "", {}, "sha512-HQQudOSQBU7ewzfnBW9LhDmBE2XOJgSfwrh5PlUB7zJup/kaRkBGNgV2wMjNz9Af/uztiU/xNrO179FysmUT+g=="], diff --git a/package.json b/package.json index b024205..af98279 100644 --- a/package.json +++ b/package.json @@ -28,20 +28,21 @@ "@elysiajs/trpc": "^1.1.0", "@trpc/server": "^10.45.2", "chalk": "^5.4.1", - "docker-compose": "^1.1.1", + "docker-compose": "^1.2.0", "dockerode": "^4.0.4", "elysia": "latest", "knip": "latest", "split2": "^4.2.0", "winston": "^3.17.0", - "yaml": "^2.7.0" + "yaml": "^2.7.1" }, "devDependencies": { - "@types/dockerode": "^3.3.34", - "@types/node": "^22.13.10", + "@types/dockerode": "^3.3.36", + "@types/node": "^22.13.14", "@types/split2": "^4.2.3", "bun-types": "latest", "cross-env": "^7.0.3", + "logform": "^2.7.0", "typescript": "^5.8.2", "wrap-ansi": "^9.0.0", "zod": "^3.24.2" diff --git a/src/core/utils/helpers.ts b/src/core/utils/helpers.ts deleted file mode 100644 index 1bc0206..0000000 --- a/src/core/utils/helpers.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { logger } from "./logger"; - -export function findObjectByKey( - array: T[], - key: keyof T, - value: T[keyof T] -): T | undefined { - const data = array.find((item) => item[key] === value); - logger.debug( - `Searching ${String(key)} = ${String(value)} in ${String( - JSON.stringify(array) - )} Found Item ${JSON.stringify(data)}` - ); - return data; -} From b2e7ebc0b1d20d26a01fc6345cb2a5a56d1a6888 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Sun, 30 Mar 2025 00:04:47 +0100 Subject: [PATCH 231/324] Fix: Remove unused files, update dependencies --- bun.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bun.lock b/bun.lock index afc9523..9a87655 100644 --- a/bun.lock +++ b/bun.lock @@ -10,17 +10,17 @@ "@elysiajs/trpc": "^1.1.0", "@trpc/server": "^10.45.2", "chalk": "^5.4.1", - "docker-compose": "^1.1.1", + "docker-compose": "^1.2.0", "dockerode": "^4.0.4", "elysia": "latest", "knip": "latest", "split2": "^4.2.0", "winston": "^3.17.0", - "yaml": "^2.7.0", + "yaml": "^2.7.1", }, "devDependencies": { - "@types/dockerode": "^3.3.34", - "@types/node": "^22.13.10", + "@types/dockerode": "^3.3.36", + "@types/node": "^22.13.14", "@types/split2": "^4.2.3", "bun-types": "latest", "cross-env": "^7.0.3", From 1ec78f660439b2483f6e2c8ee69714019f7f30e0 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Sun, 30 Mar 2025 18:54:25 +0200 Subject: [PATCH 232/324] Fix: Removing tRPC since it is not used in DockStat --- bun.lock | 7 - package.json | 5 +- src/core/trpc/README.md | 1 - src/core/trpc/index.ts | 4 - .../trpc/procedures/api-config.procedure.ts | 80 ------- .../procedures/docker-manager.procedure.ts | 65 ------ .../trpc/procedures/docker-stats.procedure.ts | 147 ------------- src/core/trpc/procedures/logs.procedure.ts | 73 ------- src/core/trpc/procedures/stacks.procedure.ts | 206 ------------------ src/core/trpc/router.ts | 19 -- src/core/trpc/trpc.ts | 5 - src/index.ts | 2 - 12 files changed, 1 insertion(+), 613 deletions(-) delete mode 100644 src/core/trpc/README.md delete mode 100644 src/core/trpc/index.ts delete mode 100644 src/core/trpc/procedures/api-config.procedure.ts delete mode 100644 src/core/trpc/procedures/docker-manager.procedure.ts delete mode 100644 src/core/trpc/procedures/docker-stats.procedure.ts delete mode 100644 src/core/trpc/procedures/logs.procedure.ts delete mode 100644 src/core/trpc/procedures/stacks.procedure.ts delete mode 100644 src/core/trpc/router.ts delete mode 100644 src/core/trpc/trpc.ts diff --git a/bun.lock b/bun.lock index 9a87655..ca73cdb 100644 --- a/bun.lock +++ b/bun.lock @@ -7,8 +7,6 @@ "@elysiajs/server-timing": "^1.2.1", "@elysiajs/static": "^1.2.0", "@elysiajs/swagger": "^1.2.2", - "@elysiajs/trpc": "^1.1.0", - "@trpc/server": "^10.45.2", "chalk": "^5.4.1", "docker-compose": "^1.2.0", "dockerode": "^4.0.4", @@ -27,7 +25,6 @@ "logform": "^2.7.0", "typescript": "^5.8.2", "wrap-ansi": "^9.0.0", - "zod": "^3.24.2", }, }, }, @@ -47,8 +44,6 @@ "@elysiajs/swagger": ["@elysiajs/swagger@1.2.2", "", { "dependencies": { "@scalar/themes": "^0.9.52", "@scalar/types": "^0.0.12", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.2.0" } }, "sha512-DG0PbX/wzQNQ6kIpFFPCvmkkWTIbNWDS7lVLv3Puy6ONklF14B4NnbDfpYjX1hdSYKeCqKBBOuenh6jKm8tbYA=="], - "@elysiajs/trpc": ["@elysiajs/trpc@1.1.0", "", { "peerDependencies": { "elysia": ">= 1.1.0" } }, "sha512-M8QWC+Wa5Z5MWY/+uMQuwZ+JoQkp4jOc1ra4SncFy1zSjFGin59LO1AT0pE+DRJaFV17gha9y7cB6Q7GnaJEAw=="], - "@grpc/grpc-js": ["@grpc/grpc-js@1.13.0", "", { "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-pMuxInZjUnUkgMT2QLZclRqwk2ykJbIU05aZgPgJYXEpN9+2I7z7aNwcjWZSycRPl232FfhPszyBFJyOxTHNog=="], "@grpc/proto-loader": ["@grpc/proto-loader@0.7.13", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.2.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw=="], @@ -91,8 +86,6 @@ "@snyk/github-codeowners": ["@snyk/github-codeowners@1.1.0", "", { "dependencies": { "commander": "^4.1.1", "ignore": "^5.1.8", "p-map": "^4.0.0" }, "bin": { "github-codeowners": "dist/cli.js" } }, "sha512-lGFf08pbkEac0NYgVf4hdANpAgApRjNByLXB+WBip3qj1iendOIyAwP2GKkKbQMNVy2r1xxDf0ssfWscoiC+Vw=="], - "@trpc/server": ["@trpc/server@10.45.2", "", {}, "sha512-wOrSThNNE4HUnuhJG6PfDRp4L2009KDVxsd+2VYH8ro6o/7/jwYZ8Uu5j+VaW+mOmc8EHerHzGcdbGNQSAUPgg=="], - "@types/docker-modem": ["@types/docker-modem@3.0.6", "", { "dependencies": { "@types/node": "*", "@types/ssh2": "*" } }, "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg=="], "@types/dockerode": ["@types/dockerode@3.3.36", "", { "dependencies": { "@types/docker-modem": "*", "@types/node": "*", "@types/ssh2": "*" } }, "sha512-K0wTBKjjVI1xS4zeLynssmmbpPl4AnWZ/MJ3JBTi9eGzEmu+xgMLVSKiWzsy/z+3GBPLD5+uE/i/6ZTeZPaX7A=="], diff --git a/package.json b/package.json index af98279..620e688 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,6 @@ "@elysiajs/server-timing": "^1.2.1", "@elysiajs/static": "^1.2.0", "@elysiajs/swagger": "^1.2.2", - "@elysiajs/trpc": "^1.1.0", - "@trpc/server": "^10.45.2", "chalk": "^5.4.1", "docker-compose": "^1.2.0", "dockerode": "^4.0.4", @@ -44,8 +42,7 @@ "cross-env": "^7.0.3", "logform": "^2.7.0", "typescript": "^5.8.2", - "wrap-ansi": "^9.0.0", - "zod": "^3.24.2" + "wrap-ansi": "^9.0.0" }, "module": "src/index.js", "trustedDependencies": [ diff --git a/src/core/trpc/README.md b/src/core/trpc/README.md deleted file mode 100644 index 32bdb3f..0000000 --- a/src/core/trpc/README.md +++ /dev/null @@ -1 +0,0 @@ -Please see: [DockStatAPI tRPC Routes Reference](https://outline.itsnik.de/s/dockstat/doc/trpc-2hzqJ7BvA0) diff --git a/src/core/trpc/index.ts b/src/core/trpc/index.ts deleted file mode 100644 index 7a13655..0000000 --- a/src/core/trpc/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { trpc } from "@elysiajs/trpc"; -import { appRouter } from "./router"; - -export default trpc(appRouter, { endpoint: "/trpc" }); diff --git a/src/core/trpc/procedures/api-config.procedure.ts b/src/core/trpc/procedures/api-config.procedure.ts deleted file mode 100644 index f517568..0000000 --- a/src/core/trpc/procedures/api-config.procedure.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { dbFunctions } from "~/core/database"; -import { logger } from "~/core/utils/logger"; -import { - version, - authorEmail, - authorName, - authorWebsite, - contributors, - dependencies, - description, - devDependencies, - license, -} from "~/core/utils/package-json"; -import { TRPCError } from "@trpc/server"; -import { z } from "zod"; -import { router, publicProcedure } from "../trpc"; -import { config } from "~/typings/database"; - -const configInputSchema = z.object({ - fetching_interval: z.number(), - keep_data_for: z.number(), - api_key: z.string(), -}); - -export const configProcedure = router({ - get: publicProcedure.query(() => { - try { - const data = dbFunctions.getConfig() as config[]; - const distinct = data[0]; - logger.debug("tRPC: Fetched backend config"); - return distinct; - } catch (error) { - logger.error("tRPC config get error", error); - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Error getting the DockStatAPI config", - cause: error, - }); - } - }), - - update: publicProcedure.input(configInputSchema).mutation(({ input }) => { - try { - const { fetching_interval, keep_data_for, api_key } = input; - dbFunctions.updateConfig(fetching_interval, keep_data_for, api_key); - return { success: true, message: "Updated DockStatAPI config" }; - } catch (error) { - logger.error("tRPC config update error", error); - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Error updating the DockStatAPI config", - cause: error, - }); - } - }), - - package: publicProcedure.query(() => { - try { - logger.debug("tRPC: Fetching package.json"); - return { - version, - description, - license, - authorName, - authorEmail, - authorWebsite, - contributors, - dependencies, - devDependencies, - }; - } catch (error) { - logger.error("tRPC package info error", error); - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Error while reading package.json", - cause: error, - }); - } - }), -}); diff --git a/src/core/trpc/procedures/docker-manager.procedure.ts b/src/core/trpc/procedures/docker-manager.procedure.ts deleted file mode 100644 index a91e769..0000000 --- a/src/core/trpc/procedures/docker-manager.procedure.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { dbFunctions } from "~/core/database"; -import { logger } from "~/core/utils/logger"; -import { TRPCError } from "@trpc/server"; -import { z } from "zod"; -import { router, publicProcedure } from "../trpc"; -import { DockerHost } from "~/typings/docker"; - -const addHostInput = z.object({ - name: z.string(), - hostAddress: z.string(), - secure: z.boolean(), -}); - -const updateHostInput = z.object({ - name: z.string(), - hostAddress: z.string(), - secure: z.boolean(), - id: z.number(), -}); - -export const dockerManagerProcedure = router({ - addHost: publicProcedure.input(addHostInput).mutation(({ input }) => { - try { - dbFunctions.addDockerHost(input as DockerHost); - logger.debug(`Added docker host (${input.name})`); - return { success: true, message: `Added docker host (${input.name})` }; - } catch (error) { - logger.error("Error adding docker host", error); - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Error adding docker host", - cause: error, - }); - } - }), - - updateHost: publicProcedure.input(updateHostInput).mutation(({ input }) => { - try { - dbFunctions.updateDockerHost(input); - return { success: true, message: `Updated docker host (${name})` }; - } catch (error) { - logger.error("Error updating docker host", error); - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Failed to update host", - cause: error, - }); - } - }), - - getHosts: publicProcedure.query(() => { - try { - const dockerHosts = dbFunctions.getDockerHosts(); - logger.debug("Retrieved docker hosts via tRPC"); - return dockerHosts; - } catch (error) { - logger.error("Error retrieving docker hosts", error); - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Failed to retrieve hosts", - cause: error, - }); - } - }), -}); diff --git a/src/core/trpc/procedures/docker-stats.procedure.ts b/src/core/trpc/procedures/docker-stats.procedure.ts deleted file mode 100644 index 0a28929..0000000 --- a/src/core/trpc/procedures/docker-stats.procedure.ts +++ /dev/null @@ -1,147 +0,0 @@ -import Docker from "dockerode"; -import { dbFunctions } from "~/core/database"; -import { getDockerClient } from "~/core/docker/client"; -import { - calculateCpuPercent, - calculateMemoryUsage, -} from "~/core/utils/calculations"; -import { logger } from "~/core/utils/logger"; -import { TRPCError } from "@trpc/server"; -import { z } from "zod"; -import { router, publicProcedure } from "../trpc"; -import type { ContainerInfo, DockerHost, HostStats } from "~/typings/docker"; -import type { DockerInfo } from "~/typings/dockerode"; - -export const dockerStatsProcedure = router({ - getContainers: publicProcedure.query(async () => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const containers: ContainerInfo[] = []; - - await Promise.all( - hosts.map(async (host) => { - try { - const docker = getDockerClient(host); - try { - await docker.ping(); - } catch (pingError) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Docker host connection failed", - cause: pingError, - }); - } - - const hostContainers = await docker.listContainers({ all: true }); - - await Promise.all( - hostContainers.map(async (containerInfo) => { - try { - const container = docker.getContainer(containerInfo.Id); - const stats = await new Promise( - (resolve, reject) => { - container.stats({ stream: false }, (error, stats) => { - if (error) { - reject( - new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Error fetching container stats", - cause: error, - }), - ); - } - if (!stats) { - reject( - new TRPCError({ - code: "NOT_FOUND", - message: "No stats available", - }), - ); - } - resolve(stats as Docker.ContainerStats); - }); - }, - ); - - containers.push({ - id: containerInfo.Id, - hostId: host.name, - name: containerInfo.Names[0].replace(/^\//, ""), - image: containerInfo.Image, - status: containerInfo.Status, - state: containerInfo.State, - cpuUsage: calculateCpuPercent(stats), - memoryUsage: calculateMemoryUsage(stats), - }); - } catch (containerError) { - logger.error( - "Error fetching container stats", - containerError, - ); - } - }), - ); - logger.debug(`Fetched stats for ${host.name}`); - } catch (hostError) { - logger.error("Error fetching containers for host", hostError); - } - }), - ); - - logger.debug("Fetched all containers across all hosts"); - return { containers }; - } catch (error) { - logger.error("Error fetching containers", error); - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Failed to retrieve containers", - cause: error, - }); - } - }), - - getHostStats: publicProcedure - .input(z.object({ id: z.string() })) - .query(async ({ input }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const host = hosts.find((h) => h.name === input.id); - - if (!host) { - throw new TRPCError({ - code: "NOT_FOUND", - message: `Host (${input.id}) not found`, - }); - } - - const docker = getDockerClient(host); - const info: DockerInfo = await docker.info(); - - const config: HostStats = { - hostId: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - labels: info.Labels, - images: info.Images, - containers: info.Containers, - containersPaused: info.ContainersPaused, - containersRunning: info.ContainersRunning, - containersStopped: info.ContainersStopped, - }; - - logger.debug(`Fetched config for ${host.name}`); - return config; - } catch (error) { - logger.error("Error fetching host stats", error); - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Failed to retrieve host config", - cause: error, - }); - } - }), -}); diff --git a/src/core/trpc/procedures/logs.procedure.ts b/src/core/trpc/procedures/logs.procedure.ts deleted file mode 100644 index b15fc9f..0000000 --- a/src/core/trpc/procedures/logs.procedure.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { dbFunctions } from "~/core/database"; -import { logger } from "~/core/utils/logger"; -import { TRPCError } from "@trpc/server"; -import { z } from "zod"; -import { router, publicProcedure } from "../trpc"; - -const logLevelSchema = z.enum(["debug", "info", "warn", "error"]); - -export const logsProcedure = router({ - getAll: publicProcedure.query(() => { - try { - const logs = dbFunctions.getAllLogs(); - logger.debug("Retrieved all logs via tRPC"); - return logs; - } catch (error) { - logger.error("Failed to retrieve logs", error); - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Failed to retrieve logs", - cause: error, - }); - } - }), - - getByLevel: publicProcedure - .input(z.object({ level: logLevelSchema })) - .query(({ input }) => { - try { - const logs = dbFunctions.getLogsByLevel(input.level); - logger.debug(`Retrieved logs (level: ${input.level}) via tRPC`); - return logs; - } catch (error) { - logger.error("Failed to retrieve logs by level", error); - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Failed to retrieve logs by level", - cause: error, - }); - } - }), - - clearAll: publicProcedure.mutation(() => { - try { - dbFunctions.clearAllLogs(); - logger.debug("Cleared all logs via tRPC"); - return { success: true }; - } catch (error) { - logger.error("Failed to clear all logs", error); - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Could not delete all logs", - cause: error, - }); - } - }), - - clearByLevel: publicProcedure - .input(z.object({ level: logLevelSchema })) - .mutation(({ input }) => { - try { - dbFunctions.clearLogsByLevel(input.level); - logger.debug(`Cleared logs (level: ${input.level}) via tRPC`); - return { success: true }; - } catch (error) { - logger.error("Failed to clear logs by level", error); - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Could not clear logs by level", - cause: error, - }); - } - }), -}); diff --git a/src/core/trpc/procedures/stacks.procedure.ts b/src/core/trpc/procedures/stacks.procedure.ts deleted file mode 100644 index 495fd32..0000000 --- a/src/core/trpc/procedures/stacks.procedure.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { dbFunctions } from "~/core/database"; -import { logger } from "~/core/utils/logger"; -import { TRPCError } from "@trpc/server"; -import { z } from "zod"; -import { router, publicProcedure } from "../trpc"; -import { - deployStack, - stopStack, - pullStackImages, - restartStack, - getStackStatus, - startStack, -} from "~/core/stacks/controller"; - -const deployStackInput = z.object({ - compose_spec: z.any(), - name: z.string(), - version: z.number(), - automatic_reboot_on_error: z.boolean(), - isCustom: z.boolean().optional(), - image_updates: z.boolean().optional(), - source: z.string(), - stack_prefix: z.string().optional(), -}); - -const stackOperationInput = z.object({ - stack: z.any(), -}); - -const stackStatusInput = z.object({ - stack_name: z.any(), -}); - -export const stacksProcedure = router({ - deploy: publicProcedure - .input(deployStackInput) - .mutation(async ({ input }) => { - try { - const missingParams = []; - if (!input.compose_spec) { - missingParams.push("compose_spec"); - } - if (!input.automatic_reboot_on_error) { - missingParams.push("automatic_reboot_on_error"); - } - if (!input.source) { - missingParams.push("source"); - } - if (!input.name) { - missingParams.push("name"); - } - - if (missingParams.length > 0) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: `Missing values: ${missingParams.join(", ")}`, - }); - } - - await deployStack( - input.compose_spec, - input.name, - input.version, - input.source, - input.automatic_reboot_on_error, - input.isCustom || false, - input.image_updates || false, - input.stack_prefix, - ); - - logger.info(`Deployed Stack (${input.name}) via tRPC`); - return { - success: true, - message: `Stack ${input.name} deployed successfully`, - }; - } catch (error) { - logger.error("Error deploying stack", error); - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - error instanceof Error ? error.message : "Error deploying stack", - cause: error, - }); - } - }), - - start: publicProcedure - .input(stackOperationInput) - .mutation(async ({ input }) => { - try { - await startStack(input.stack); - logger.info(`Started Stack (${input.stack}) via tRPC`); - return { - success: true, - message: `Stack ${input.stack} started successfully`, - }; - } catch (error) { - logger.error("Error starting stack", error); - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - error instanceof Error ? error.message : "Error starting stack", - cause: error, - }); - } - }), - - stop: publicProcedure - .input(stackOperationInput) - .mutation(async ({ input }) => { - try { - await stopStack(input.stack); - logger.info(`Stopped Stack (${input.stack}) via tRPC`); - return { - success: true, - message: `Stack ${input.stack} stopped successfully`, - }; - } catch (error) { - logger.error("Error stopping stack", error); - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - error instanceof Error ? error.message : "Error stopping stack", - cause: error, - }); - } - }), - - restart: publicProcedure - .input(stackOperationInput) - .mutation(async ({ input }) => { - try { - await restartStack(input.stack); - logger.info(`Restarted Stack (${input.stack}) via tRPC`); - return { - success: true, - message: `Stack ${input.stack} restarted successfully`, - }; - } catch (error) { - logger.error("Error restarting stack", error); - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - error instanceof Error ? error.message : "Error restarting stack", - cause: error, - }); - } - }), - - pullImages: publicProcedure - .input(stackOperationInput) - .mutation(async ({ input }) => { - try { - await pullStackImages(input.stack); - logger.info(`Pulled Stack images (${input.stack}) via tRPC`); - return { - success: true, - message: `Images for stack ${input.stack} pulled successfully`, - }; - } catch (error) { - logger.error("Error pulling images", error); - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - error instanceof Error ? error.message : "Error pulling images", - cause: error, - }); - } - }), - - getStatus: publicProcedure - .input(stackStatusInput) - .query(async ({ input }) => { - try { - const status = await getStackStatus(input.stack_name); - logger.info(`Fetched Stack status (${input.stack_name}) via tRPC`); - return { status }; - } catch (error) { - logger.error("Error getting stack status", error); - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - error instanceof Error - ? error.message - : "Error getting stack status", - cause: error, - }); - } - }), - - getAll: publicProcedure.query(() => { - try { - const stacks = dbFunctions.getStacks(); - logger.info("Fetched Stacks via tRPC"); - return stacks; - } catch (error) { - logger.error("Error getting stacks", error); - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - error instanceof Error ? error.message : "Error getting stacks", - cause: error, - }); - } - }), -}); diff --git a/src/core/trpc/router.ts b/src/core/trpc/router.ts deleted file mode 100644 index 9e0bddf..0000000 --- a/src/core/trpc/router.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { router, t } from "./trpc"; -import { configProcedure } from "./procedures/api-config.procedure"; -import { dockerManagerProcedure } from "./procedures/docker-manager.procedure"; -import { dockerStatsProcedure } from "./procedures/docker-stats.procedure"; -import { logsProcedure } from "./procedures/logs.procedure"; -import { stacksProcedure } from "./procedures/stacks.procedure"; - -export const appRouter = router({ - config: configProcedure, - docker: router({ - manager: dockerManagerProcedure, - stats: dockerStatsProcedure, - }), - logs: logsProcedure, - stacks: stacksProcedure, - health: router({ - check: t.procedure.query(() => ({ status: "healthy" })), - }), -}); diff --git a/src/core/trpc/trpc.ts b/src/core/trpc/trpc.ts deleted file mode 100644 index c7813f9..0000000 --- a/src/core/trpc/trpc.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { initTRPC } from "@trpc/server"; - -export const t = initTRPC.create(); -export const { router } = t; -export const publicProcedure = t.procedure; diff --git a/src/index.ts b/src/index.ts index 5cc5af3..16cd5ee 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,7 +12,6 @@ import { apiConfigRoutes } from "~/routes/api-config"; import { setSchedules } from "~/core/docker/scheduler"; import { serverTiming } from "@elysiajs/server-timing"; import staticPlugin from "@elysiajs/static"; -import trpcRouter from "~/core/trpc"; import { config } from "./typings/database"; import { validateApiKey } from "./middleware/auth"; import { monitorDockerEvents } from "./core/docker/monitor"; @@ -88,7 +87,6 @@ export const DockStatAPI = new Elysia() return { error: validation.error }; } }) - .use(trpcRouter) .use(dockerRoutes) .use(dockerStatsRoutes) .use(backendLogs) From 28760486ada9658bdc43ff2b98b153c2e8c8c82b Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Sun, 30 Mar 2025 16:54:59 +0000 Subject: [PATCH 233/324] Update dependency graphs --- dependency-graph.dot | 39 -- dependency-graph.mmd | 117 ++-- dependency-graph.svg | 1286 +++++++++++++++++------------------------- 3 files changed, 546 insertions(+), 896 deletions(-) diff --git a/dependency-graph.dot b/dependency-graph.dot index a3cdc90..7c5bbef 100644 --- a/dependency-graph.dot +++ b/dependency-graph.dot @@ -91,44 +91,6 @@ strict digraph "dependency-cruiser output"{ "src/core/stacks/controller.ts" -> "src/typings/docker-compose.ts" [arrowhead="onormal" penwidth="1.0"] "src/core/stacks/controller.ts" -> "bun" "src/core/stacks/controller.ts" -> "fs/promises" [style="dashed" penwidth="1.0"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" "src/core/trpc/index.ts" [label= tooltip="index.ts" URL="src/core/trpc/index.ts" fillcolor="#ddfeff"] } } } - "src/core/trpc/index.ts" -> "src/core/trpc/router.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/api-config.procedure.ts" [label= tooltip="api-config.procedure.ts" URL="src/core/trpc/procedures/api-config.procedure.ts" fillcolor="#ddfeff"] } } } } - "src/core/trpc/procedures/api-config.procedure.ts" -> "src/core/trpc/trpc.ts" - "src/core/trpc/procedures/api-config.procedure.ts" -> "src/core/database/index.ts" - "src/core/trpc/procedures/api-config.procedure.ts" -> "src/core/utils/logger.ts" - "src/core/trpc/procedures/api-config.procedure.ts" -> "src/core/utils/package-json.ts" - "src/core/trpc/procedures/api-config.procedure.ts" -> "src/typings/database.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/docker-manager.procedure.ts" [label= tooltip="docker-manager.procedure.ts" URL="src/core/trpc/procedures/docker-manager.procedure.ts" fillcolor="#ddfeff"] } } } } - "src/core/trpc/procedures/docker-manager.procedure.ts" -> "src/core/trpc/trpc.ts" - "src/core/trpc/procedures/docker-manager.procedure.ts" -> "src/core/database/index.ts" - "src/core/trpc/procedures/docker-manager.procedure.ts" -> "src/core/utils/logger.ts" - "src/core/trpc/procedures/docker-manager.procedure.ts" -> "src/typings/docker.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/docker-stats.procedure.ts" [label= tooltip="docker-stats.procedure.ts" URL="src/core/trpc/procedures/docker-stats.procedure.ts" fillcolor="#ddfeff"] } } } } - "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/trpc/trpc.ts" - "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/database/index.ts" - "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/docker/client.ts" - "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/utils/calculations.ts" - "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/utils/logger.ts" - "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] - "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/typings/dockerode.ts" [arrowhead="onormal" penwidth="1.0"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/logs.procedure.ts" [label= tooltip="logs.procedure.ts" URL="src/core/trpc/procedures/logs.procedure.ts" fillcolor="#ddfeff"] } } } } - "src/core/trpc/procedures/logs.procedure.ts" -> "src/core/trpc/trpc.ts" - "src/core/trpc/procedures/logs.procedure.ts" -> "src/core/database/index.ts" - "src/core/trpc/procedures/logs.procedure.ts" -> "src/core/utils/logger.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/stacks.procedure.ts" [label= tooltip="stacks.procedure.ts" URL="src/core/trpc/procedures/stacks.procedure.ts" fillcolor="#ddfeff"] } } } } - "src/core/trpc/procedures/stacks.procedure.ts" -> "src/core/trpc/trpc.ts" - "src/core/trpc/procedures/stacks.procedure.ts" -> "src/core/database/index.ts" - "src/core/trpc/procedures/stacks.procedure.ts" -> "src/core/stacks/controller.ts" - "src/core/trpc/procedures/stacks.procedure.ts" -> "src/core/utils/logger.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" "src/core/trpc/router.ts" [label= tooltip="router.ts" URL="src/core/trpc/router.ts" fillcolor="#ddfeff"] } } } - "src/core/trpc/router.ts" -> "src/core/trpc/procedures/api-config.procedure.ts" - "src/core/trpc/router.ts" -> "src/core/trpc/procedures/docker-manager.procedure.ts" - "src/core/trpc/router.ts" -> "src/core/trpc/procedures/docker-stats.procedure.ts" - "src/core/trpc/router.ts" -> "src/core/trpc/procedures/logs.procedure.ts" - "src/core/trpc/router.ts" -> "src/core/trpc/procedures/stacks.procedure.ts" - "src/core/trpc/router.ts" -> "src/core/trpc/trpc.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" "src/core/trpc/trpc.ts" [label= tooltip="trpc.ts" URL="src/core/trpc/trpc.ts" fillcolor="#ddfeff"] } } } subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/calculations.ts" [label= tooltip="calculations.ts" URL="src/core/utils/calculations.ts" fillcolor="#ddfeff"] } } } subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/change-me-checker.ts" [label= tooltip="change-me-checker.ts" URL="src/core/utils/change-me-checker.ts" fillcolor="#ddfeff"] } } } "src/core/utils/change-me-checker.ts" -> "src/core/utils/logger.ts" @@ -155,7 +117,6 @@ strict digraph "dependency-cruiser output"{ "src/index.ts" -> "src/core/database/index.ts" "src/index.ts" -> "src/core/docker/scheduler.ts" "src/index.ts" -> "src/core/plugins/loader.ts" - "src/index.ts" -> "src/core/trpc/index.ts" "src/index.ts" -> "src/core/utils/logger.ts" "src/index.ts" -> "src/routes/api-config.ts" "src/index.ts" -> "src/routes/docker-manager.ts" diff --git a/dependency-graph.mmd b/dependency-graph.mmd index bbe9dd9..79af901 100644 --- a/dependency-graph.mmd +++ b/dependency-graph.mmd @@ -42,18 +42,6 @@ end subgraph 11["stacks"] 12["controller.ts"] end -subgraph 1G["trpc"] -1H["index.ts"] -1I["router.ts"] -subgraph 1J["procedures"] -1K["api-config.procedure.ts"] -1M["docker-manager.procedure.ts"] -1N["docker-stats.procedure.ts"] -1O["logs.procedure.ts"] -1P["stacks.procedure.ts"] -end -1L["trpc.ts"] -end end subgraph K["typings"] L["docker.ts"] @@ -68,11 +56,11 @@ subgraph S["routes"] T["live-logs.ts"] 10["stacks.ts"] 16["utils.ts"] -1Q["api-config.ts"] -1R["docker-manager.ts"] -1S["docker-stats.ts"] -1T["docker-websocket.ts"] -1V["logs.ts"] +1G["api-config.ts"] +1H["docker-manager.ts"] +1I["docker-stats.ts"] +1J["docker-websocket.ts"] +1L["logs.ts"] end subgraph X["middleware"] Y["auth.ts"] @@ -86,7 +74,7 @@ subgraph 13["fs"] 14["promises"] end 18["package.json"] -1U["stream"] +1K["stream"] 1-->4 1-->W 1-->Y @@ -97,13 +85,12 @@ end 1-->D 1-->19 1-->1E -1-->1H 1-->A -1-->1Q -1-->1R -1-->1S -1-->1T -1-->1V +1-->1G +1-->1H +1-->1I +1-->1J +1-->1L 4-->7 4-->D 4-->V @@ -190,60 +177,30 @@ Y-->Z 1E-->B 1F-->A 1F-->14 -1H-->1I -1I-->1K -1I-->1M -1I-->1N -1I-->1O -1I-->1P -1I-->1L -1K-->1L -1K-->D -1K-->A -1K-->17 -1K-->Q -1M-->1L -1M-->D -1M-->A -1M-->L -1N-->1L -1N-->D -1N-->V -1N-->1D -1N-->A -1N-->L -1N-->1B -1O-->1L -1O-->D -1O-->A -1P-->1L -1P-->D -1P-->12 -1P-->A -1Q-->D -1Q-->7 -1Q-->A -1Q-->17 -1Q-->15 -1Q-->Y -1Q-->Q -1R-->D -1R-->A -1R-->15 -1R-->L -1S-->D -1S-->V -1S-->1D -1S-->A -1S-->15 -1S-->L -1S-->1B -1T-->D -1T-->V -1T-->1D -1T-->A -1T-->15 -1T-->1U -1V-->D -1V-->A +1G-->D +1G-->7 +1G-->A +1G-->17 +1G-->15 +1G-->Y +1G-->Q +1H-->D +1H-->A +1H-->15 +1H-->L +1I-->D +1I-->V +1I-->1D +1I-->A +1I-->15 +1I-->L +1I-->1B +1J-->D +1J-->V +1J-->1D +1J-->A +1J-->15 +1J-->1K +1L-->D +1L-->A diff --git a/dependency-graph.svg b/dependency-graph.svg index c1e4807..66611d2 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -4,82 +4,72 @@ - - + + dependency-cruiser output - + cluster_fs - -fs + +fs cluster_src - -src + +src cluster_src/core - -core + +core cluster_src/core/database - -database + +database cluster_src/core/docker - -docker + +docker cluster_src/core/plugins - -plugins + +plugins cluster_src/core/stacks - -stacks + +stacks -cluster_src/core/trpc - -trpc - - -cluster_src/core/trpc/procedures - -procedures - - cluster_src/core/utils - -utils + +utils - + cluster_src/middleware - -middleware + +middleware - + cluster_src/routes - -routes + +routes - + cluster_src/typings - -typings + +typings bun - -bun + +bun @@ -87,8 +77,8 @@ bun:sqlite - -bun:sqlite + +bun:sqlite @@ -96,8 +86,8 @@ events - -events + +events @@ -105,8 +95,8 @@ fs - -fs + +fs @@ -114,8 +104,8 @@ fs/promises - -promises + +promises @@ -123,8 +113,8 @@ package.json - -package.json + +package.json @@ -132,8 +122,8 @@ path - -path + +path @@ -141,8 +131,8 @@ src/core/database/config.ts - -config.ts + +config.ts @@ -150,1413 +140,1155 @@ src/core/database/database.ts - -database.ts + +database.ts src/core/database/config.ts->src/core/database/database.ts - - + + src/core/database/helper.ts - -helper.ts + +helper.ts src/core/database/config.ts->src/core/database/helper.ts - - - - + + + + src/core/database/database.ts->bun:sqlite - - + + src/core/utils/logger.ts - -logger.ts + +logger.ts src/core/database/helper.ts->src/core/utils/logger.ts - - - - + + + + src/core/database/containerStats.ts - -containerStats.ts + +containerStats.ts src/core/database/containerStats.ts->src/core/database/database.ts - - + + src/core/database/containerStats.ts->src/core/database/helper.ts - - - - + + + + src/core/database/dockerHosts.ts - -dockerHosts.ts + +dockerHosts.ts src/core/database/dockerHosts.ts->src/core/database/database.ts - - + + src/core/database/dockerHosts.ts->src/core/database/helper.ts - - - - + + + + src/typings/docker.ts - -docker.ts + +docker.ts src/core/database/dockerHosts.ts->src/typings/docker.ts - - + + - + src/core/utils/logger.ts->path - - + + src/core/database/index.ts - -index.ts + +index.ts - + src/core/utils/logger.ts->src/core/database/index.ts - - - - + + + + src/typings/websocket.ts - -websocket.ts + +websocket.ts - + src/core/utils/logger.ts->src/typings/websocket.ts - - + + - + src/routes/live-logs.ts - - -live-logs.ts + + +live-logs.ts - + src/core/utils/logger.ts->src/routes/live-logs.ts - - - - + + + + src/core/database/hostStats.ts - -hostStats.ts + +hostStats.ts src/core/database/hostStats.ts->src/core/database/database.ts - - + + src/core/database/hostStats.ts->src/core/database/helper.ts - - - - + + + + src/core/database/hostStats.ts->src/typings/docker.ts - - + + src/core/database/index.ts->src/core/database/config.ts - - - - + + + + src/core/database/index.ts->src/core/database/database.ts - - + + src/core/database/index.ts->src/core/database/containerStats.ts - - - - + + + + src/core/database/index.ts->src/core/database/dockerHosts.ts - - - - + + + + src/core/database/index.ts->src/core/database/hostStats.ts - - - - + + + + src/core/database/logs.ts - -logs.ts + +logs.ts src/core/database/index.ts->src/core/database/logs.ts - - - - + + + + src/core/database/stacks.ts - -stacks.ts + +stacks.ts src/core/database/index.ts->src/core/database/stacks.ts - - - - + + + + src/core/database/logs.ts->src/core/database/database.ts - - + + src/core/database/logs.ts->src/core/database/helper.ts - - - - + + + + src/core/database/logs.ts->src/typings/websocket.ts - - + + src/core/database/stacks.ts->src/core/database/database.ts - - + + src/core/database/stacks.ts->src/core/database/helper.ts - - - - + + + + src/typings/database.ts - -database.ts + +database.ts src/core/database/stacks.ts->src/typings/database.ts - - + + src/typings/docker-compose.ts - -docker-compose.ts + +docker-compose.ts src/core/database/stacks.ts->src/typings/docker-compose.ts - - + + src/core/docker/client.ts - -client.ts + +client.ts src/core/docker/client.ts->src/typings/docker.ts - - + + src/core/docker/client.ts->src/core/utils/logger.ts - - + + src/core/docker/monitor.ts - -monitor.ts + +monitor.ts src/core/docker/monitor.ts->bun - - + + src/core/docker/monitor.ts->src/typings/docker.ts - - + + src/core/docker/monitor.ts->src/core/utils/logger.ts - - + + src/core/docker/monitor.ts->src/core/database/index.ts - - + + src/core/docker/monitor.ts->src/core/docker/client.ts - - + + src/core/plugins/plugin-manager.ts - -plugin-manager.ts + +plugin-manager.ts src/core/docker/monitor.ts->src/core/plugins/plugin-manager.ts - - + + src/core/plugins/plugin-manager.ts->events - - + + src/core/plugins/plugin-manager.ts->src/typings/docker.ts - - + + src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts - - + + src/typings/plugin.ts - -plugin.ts + +plugin.ts src/core/plugins/plugin-manager.ts->src/typings/plugin.ts - - + + src/core/docker/scheduler.ts - -scheduler.ts + +scheduler.ts src/core/docker/scheduler.ts->src/core/utils/logger.ts - - + + src/core/docker/scheduler.ts->src/core/database/index.ts - - + + src/core/docker/scheduler.ts->src/typings/database.ts - - + + src/core/docker/store-host-stats.ts - -store-host-stats.ts + +store-host-stats.ts src/core/docker/scheduler.ts->src/core/docker/store-host-stats.ts - - + + src/core/docker/store-container-stats.ts - -store-container-stats.ts + +store-container-stats.ts src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts - - + + src/core/docker/store-host-stats.ts->src/typings/docker.ts - - + + src/core/docker/store-host-stats.ts->src/core/utils/logger.ts - - + + src/core/docker/store-host-stats.ts->src/core/database/index.ts - - + + src/core/docker/store-host-stats.ts->src/core/docker/client.ts - - + + src/typings/dockerode.ts - -dockerode.ts + +dockerode.ts src/core/docker/store-host-stats.ts->src/typings/dockerode.ts - - + + src/core/docker/store-container-stats.ts->src/core/utils/logger.ts - - + + src/core/docker/store-container-stats.ts->src/core/database/index.ts - - + + src/core/docker/store-container-stats.ts->src/core/docker/client.ts - - + + src/core/utils/calculations.ts - -calculations.ts + +calculations.ts src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts - - + + src/core/plugins/loader.ts - -loader.ts + +loader.ts src/core/plugins/loader.ts->fs - - + + src/core/plugins/loader.ts->path - - + + src/core/plugins/loader.ts->src/core/utils/logger.ts - - + + src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts - - + + src/core/utils/change-me-checker.ts - -change-me-checker.ts + +change-me-checker.ts src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts - - + + - + src/core/utils/change-me-checker.ts->fs/promises - - + + - + src/core/utils/change-me-checker.ts->src/core/utils/logger.ts - - + + - + src/typings/plugin.ts->src/typings/docker.ts - - + + src/core/stacks/controller.ts - -controller.ts + +controller.ts src/core/stacks/controller.ts->bun - - + + src/core/stacks/controller.ts->fs/promises - - + + src/core/stacks/controller.ts->src/core/utils/logger.ts - - + + src/core/stacks/controller.ts->src/core/database/index.ts - - + + src/core/stacks/controller.ts->src/typings/database.ts - - + + src/core/stacks/controller.ts->src/typings/docker-compose.ts - - - - - -src/core/trpc/index.ts - - -index.ts - - - - - -src/core/trpc/router.ts - - -router.ts - - - - - -src/core/trpc/index.ts->src/core/trpc/router.ts - - - - - -src/core/trpc/procedures/api-config.procedure.ts - - -api-config.procedure.ts - - + + - - -src/core/trpc/router.ts->src/core/trpc/procedures/api-config.procedure.ts - - - - - -src/core/trpc/trpc.ts - - -trpc.ts - - - - - -src/core/trpc/router.ts->src/core/trpc/trpc.ts - - - - - -src/core/trpc/procedures/docker-manager.procedure.ts - - -docker-manager.procedure.ts - - - - - -src/core/trpc/router.ts->src/core/trpc/procedures/docker-manager.procedure.ts - - - - - -src/core/trpc/procedures/docker-stats.procedure.ts - - -docker-stats.procedure.ts - - - - - -src/core/trpc/router.ts->src/core/trpc/procedures/docker-stats.procedure.ts - - - - - -src/core/trpc/procedures/logs.procedure.ts - - -logs.procedure.ts - - - - - -src/core/trpc/router.ts->src/core/trpc/procedures/logs.procedure.ts - - - - - -src/core/trpc/procedures/stacks.procedure.ts - - -stacks.procedure.ts - - - - - -src/core/trpc/router.ts->src/core/trpc/procedures/stacks.procedure.ts - - - - - -src/core/trpc/procedures/api-config.procedure.ts->src/core/utils/logger.ts - - - - - -src/core/trpc/procedures/api-config.procedure.ts->src/core/database/index.ts - - - - - -src/core/trpc/procedures/api-config.procedure.ts->src/typings/database.ts - - + + +src/routes/live-logs.ts->src/core/utils/logger.ts + + + + - - -src/core/trpc/procedures/api-config.procedure.ts->src/core/trpc/trpc.ts - - + + +src/routes/live-logs.ts->src/typings/websocket.ts + + - + src/core/utils/package-json.ts - - -package-json.ts + + +package-json.ts - - -src/core/trpc/procedures/api-config.procedure.ts->src/core/utils/package-json.ts - - - - -src/core/utils/package-json.ts->package.json - - - - - -src/core/trpc/procedures/docker-manager.procedure.ts->src/typings/docker.ts - - - - - -src/core/trpc/procedures/docker-manager.procedure.ts->src/core/utils/logger.ts - - - - - -src/core/trpc/procedures/docker-manager.procedure.ts->src/core/database/index.ts - - - - -src/core/trpc/procedures/docker-manager.procedure.ts->src/core/trpc/trpc.ts - - - - - -src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/docker.ts - - - - - -src/core/trpc/procedures/docker-stats.procedure.ts->src/core/utils/logger.ts - - - - - -src/core/trpc/procedures/docker-stats.procedure.ts->src/core/database/index.ts - - - - - -src/core/trpc/procedures/docker-stats.procedure.ts->src/core/docker/client.ts - - - - - -src/core/trpc/procedures/docker-stats.procedure.ts->src/core/utils/calculations.ts - - - - - -src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/dockerode.ts - - - - - -src/core/trpc/procedures/docker-stats.procedure.ts->src/core/trpc/trpc.ts - - - - - -src/core/trpc/procedures/logs.procedure.ts->src/core/utils/logger.ts - - - - - -src/core/trpc/procedures/logs.procedure.ts->src/core/database/index.ts - - - - - -src/core/trpc/procedures/logs.procedure.ts->src/core/trpc/trpc.ts - - - - - -src/core/trpc/procedures/stacks.procedure.ts->src/core/utils/logger.ts - - - - - -src/core/trpc/procedures/stacks.procedure.ts->src/core/database/index.ts - - - - - -src/core/trpc/procedures/stacks.procedure.ts->src/core/stacks/controller.ts - - - - - -src/core/trpc/procedures/stacks.procedure.ts->src/core/trpc/trpc.ts - - - - - -src/routes/live-logs.ts->src/core/utils/logger.ts - - - - - - - -src/routes/live-logs.ts->src/typings/websocket.ts - - +src/core/utils/package-json.ts->package.json + + - + src/core/utils/response-handler.ts - - -response-handler.ts + + +response-handler.ts - + src/core/utils/response-handler.ts->src/core/utils/logger.ts - - + + - + src/typings/elysiajs.ts - - -elysiajs.ts + + +elysiajs.ts - + src/core/utils/response-handler.ts->src/typings/elysiajs.ts - - + + - + src/core/utils/swagger-readme.ts - - -swagger-readme.ts + + +swagger-readme.ts - + src/index.ts - - -index.ts + + +index.ts - + src/index.ts->src/core/utils/logger.ts - - + + - + src/index.ts->src/core/database/index.ts - - + + - + src/index.ts->src/typings/database.ts - - + + - + src/index.ts->src/core/docker/monitor.ts - - + + - + src/index.ts->src/core/docker/scheduler.ts - - + + - + src/index.ts->src/core/plugins/loader.ts - - - - - -src/index.ts->src/core/trpc/index.ts - - + + - + src/index.ts->src/routes/live-logs.ts - - + + - + src/index.ts->src/core/utils/swagger-readme.ts - - + + - + src/middleware/auth.ts - - -auth.ts + + +auth.ts - + src/index.ts->src/middleware/auth.ts - - + + - + src/routes/stacks.ts - - -stacks.ts + + +stacks.ts - + src/index.ts->src/routes/stacks.ts - - + + - + src/routes/utils.ts - - -utils.ts + + +utils.ts - + src/index.ts->src/routes/utils.ts - - + + - + src/routes/api-config.ts - - -api-config.ts + + +api-config.ts - + src/index.ts->src/routes/api-config.ts - - + + - + src/routes/docker-manager.ts - - -docker-manager.ts + + +docker-manager.ts - + src/index.ts->src/routes/docker-manager.ts - - + + - + src/routes/docker-stats.ts - - -docker-stats.ts + + +docker-stats.ts - + src/index.ts->src/routes/docker-stats.ts - - + + - + src/routes/docker-websocket.ts - - -docker-websocket.ts + + +docker-websocket.ts - + src/index.ts->src/routes/docker-websocket.ts - - + + - + src/routes/logs.ts - - -logs.ts + + +logs.ts - + src/index.ts->src/routes/logs.ts - - + + - + src/middleware/auth.ts->src/core/utils/logger.ts - - + + - + src/middleware/auth.ts->src/core/database/index.ts - - + + - + src/middleware/auth.ts->src/typings/database.ts - - + + - + src/middleware/auth.ts->src/typings/elysiajs.ts - - + + - + src/routes/stacks.ts->src/core/utils/logger.ts - - + + - + src/routes/stacks.ts->src/core/database/index.ts - - + + - + src/routes/stacks.ts->src/core/stacks/controller.ts - - + + - + src/routes/stacks.ts->src/core/utils/response-handler.ts - - + + - + src/routes/utils.ts->src/core/utils/package-json.ts - - + + - + src/routes/utils.ts->src/core/utils/response-handler.ts - - + + - + src/routes/api-config.ts->src/core/utils/logger.ts - - + + - + src/routes/api-config.ts->src/core/database/index.ts - - + + - + src/routes/api-config.ts->src/typings/database.ts - - + + - + src/routes/api-config.ts->src/core/plugins/plugin-manager.ts - - + + - + src/routes/api-config.ts->src/core/utils/package-json.ts - - + + - + src/routes/api-config.ts->src/core/utils/response-handler.ts - - + + - + src/routes/api-config.ts->src/middleware/auth.ts - - + + - + src/routes/docker-manager.ts->src/typings/docker.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-manager.ts->src/core/database/index.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/response-handler.ts - - + + - + src/routes/docker-stats.ts->src/typings/docker.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-stats.ts->src/core/database/index.ts - - + + - + src/routes/docker-stats.ts->src/core/docker/client.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-stats.ts->src/typings/dockerode.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/response-handler.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-websocket.ts->src/core/database/index.ts - - + + - + src/routes/docker-websocket.ts->src/core/docker/client.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/response-handler.ts - - + + - + stream - - -stream + + +stream - + src/routes/docker-websocket.ts->stream - - + + - + src/routes/logs.ts->src/core/utils/logger.ts - - + + - + src/routes/logs.ts->src/core/database/index.ts - - + + From a8506c57a072de71793ae51b6bbaf37804912a58 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Sun, 30 Mar 2025 19:32:20 +0200 Subject: [PATCH 234/324] Fix: Minor code adjustments --- .github/workflows/dependency-graph.yml | 2 +- .gitignore | 3 ++- src/core/database/dockerHosts.ts | 15 ++++++++++----- src/core/docker/store-host-stats.ts | 3 ++- src/core/stacks/controller.ts | 4 ++-- src/core/utils/helpers.ts | 15 +++++++++++++++ src/routes/docker-stats.ts | 4 ++-- src/tests/post.spec.ts | 6 +++--- src/typings/docker.ts | 2 +- 9 files changed, 38 insertions(+), 16 deletions(-) create mode 100644 src/core/utils/helpers.ts diff --git a/.github/workflows/dependency-graph.yml b/.github/workflows/dependency-graph.yml index 03d23f1..4fad400 100644 --- a/.github/workflows/dependency-graph.yml +++ b/.github/workflows/dependency-graph.yml @@ -38,7 +38,7 @@ jobs: - name: Commit and Push Changes uses: EndBug/add-and-commit@v9 with: - add: "dependency-graph*" + add: '["dependency-graph.svg", "dependency-graph.mmd"]' message: "Update dependency graphs" committer_name: "GitHub Action" committer_email: "action@github.com" diff --git a/.gitignore b/.gitignore index c61c683..c7aba1f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.db* /stacks /node_modules -.test \ No newline at end of file +.test +dependency-graph* diff --git a/src/core/database/dockerHosts.ts b/src/core/database/dockerHosts.ts index bdaf2d1..c805703 100644 --- a/src/core/database/dockerHosts.ts +++ b/src/core/database/dockerHosts.ts @@ -29,12 +29,17 @@ export function addDockerHost(host: DockerHost) { } export function getDockerHosts(): DockerHost[] { - return executeDbOperation( - "Get Docker Hosts", - () => stmt.selectAll.all() as DockerHost[], - ); + return executeDbOperation("Get Docker Hosts", () => { + const rows = stmt.selectAll.all() as Array< + Omit & { secure: number } + >; + return rows.map((row) => ({ + ...row, + secure: row.secure === 1, + })); + }); } - +1; export function updateDockerHost(host: DockerHost) { return executeDbOperation( "Update Docker Host", diff --git a/src/core/docker/store-host-stats.ts b/src/core/docker/store-host-stats.ts index 9536bc1..fa7b05d 100644 --- a/src/core/docker/store-host-stats.ts +++ b/src/core/docker/store-host-stats.ts @@ -3,10 +3,11 @@ import { dbFunctions } from "~/core/database"; import { DockerHost, HostStats } from "~/typings/docker"; import { getDockerClient } from "~/core/docker/client"; import { DockerInfo } from "~/typings/dockerode"; +import { findObjectByKey } from "~/core/utils/helpers"; function getHostByName(hostName: string): DockerHost { const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const foundHost = hosts.find((host) => host.name === hostName); + const foundHost = findObjectByKey(hosts, "name", hostName); if (!foundHost) { throw new Error(`Host ${hostName} not found`); } diff --git a/src/core/stacks/controller.ts b/src/core/stacks/controller.ts index 838532c..1c85b59 100644 --- a/src/core/stacks/controller.ts +++ b/src/core/stacks/controller.ts @@ -5,12 +5,12 @@ import DockerCompose from "docker-compose"; import type { Stack, ComposeSpec } from "~/typings/docker-compose"; import type { stacks_config } from "~/typings/database"; import { rm } from "node:fs/promises"; -import { ErrorLike } from "bun"; +import { findObjectByKey } from "../utils/helpers"; async function getStackName(stack_id: number): Promise { logger.debug(`Fetching stack name for id ${stack_id}`); const stacks = dbFunctions.getStacks(); - const stack = stacks.find((stack) => Number(stack.id) === Number(stack_id)); + const stack = findObjectByKey(stacks, "id", stack_id); if (!stack) { throw new Error(`Stack with id ${stack_id} not found`); } diff --git a/src/core/utils/helpers.ts b/src/core/utils/helpers.ts new file mode 100644 index 0000000..7f3a99c --- /dev/null +++ b/src/core/utils/helpers.ts @@ -0,0 +1,15 @@ +import { logger } from "./logger"; + +export function findObjectByKey( + array: T[], + key: keyof T, + value: T[keyof T], +): T | undefined { + const data = array.find((item) => item[key] === value); + logger.debug( + `Searching ${String(key)} = ${String(value)} in ${String( + JSON.stringify(array), + )} Found Item ${JSON.stringify(data)}`, + ); + return data; +} diff --git a/src/routes/docker-stats.ts b/src/routes/docker-stats.ts index a600af3..b6bdd3f 100644 --- a/src/routes/docker-stats.ts +++ b/src/routes/docker-stats.ts @@ -8,6 +8,7 @@ import { } from "~/core/utils/calculations"; import { logger } from "~/core/utils/logger"; import { responseHandler } from "~/core/utils/response-handler"; +import { findObjectByKey } from "~/core/utils/helpers"; import type { ContainerInfo, DockerHost, HostStats } from "~/typings/docker"; import type { DockerInfo } from "~/typings/dockerode"; @@ -112,8 +113,7 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) async ({ params, set }) => { try { const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const host = hosts.find((h) => h.name === params.id); - + const host = findObjectByKey(hosts, "name", params.id); if (!host) { return responseHandler.simple_error( set, diff --git a/src/tests/post.spec.ts b/src/tests/post.spec.ts index 040012e..9ce64e9 100644 --- a/src/tests/post.spec.ts +++ b/src/tests/post.spec.ts @@ -25,18 +25,18 @@ describe("DockStatAPI (POST)", () => { await runTestCode("/docker-config/update-host", 200, "POST", codeBody); const responseBody: DockerHost[] = [ - { id: 2, name: "test", hostAddress: "127.0.0.1:2375", secure: 0 }, + { id: 2, name: "test", hostAddress: "127.0.0.1:2375", secure: false }, { id: 1, name: "Localhost", hostAddress: "localhost:2375", - secure: 0, + secure: false, }, ]; await runTestResponse( "/docker-config/hosts", JSON.stringify(responseBody), - "GET" + "GET", ); }); diff --git a/src/typings/docker.ts b/src/typings/docker.ts index 0d759ba..8b4f503 100644 --- a/src/typings/docker.ts +++ b/src/typings/docker.ts @@ -1,7 +1,7 @@ interface DockerHost { name: string; hostAddress: string; - secure: boolean | number; + secure: boolean; id: number; } From ad3509e59ecd720106c7d878713d9c18b33537d4 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Sun, 30 Mar 2025 17:34:05 +0000 Subject: [PATCH 235/324] Update dependency graphs --- dependency-graph.mmd | 128 ++--- dependency-graph.svg | 1077 ++++++++++++++++++++++-------------------- 2 files changed, 618 insertions(+), 587 deletions(-) diff --git a/dependency-graph.mmd b/dependency-graph.mmd index 79af901..844ace0 100644 --- a/dependency-graph.mmd +++ b/dependency-graph.mmd @@ -12,21 +12,22 @@ subgraph 2["core"] subgraph 3["docker"] 4["monitor.ts"] V["client.ts"] -19["scheduler.ts"] -1A["store-host-stats.ts"] -1C["store-container-stats.ts"] +1A["scheduler.ts"] +1B["store-host-stats.ts"] +1D["store-container-stats.ts"] end subgraph 6["plugins"] 7["plugin-manager.ts"] -1E["loader.ts"] +1F["loader.ts"] end subgraph 9["utils"] A["logger.ts"] W["swagger-readme.ts"] -15["response-handler.ts"] -17["package-json.ts"] -1D["calculations.ts"] -1F["change-me-checker.ts"] +15["helpers.ts"] +16["response-handler.ts"] +18["package-json.ts"] +1E["calculations.ts"] +1G["change-me-checker.ts"] end subgraph C["database"] D["index.ts"] @@ -50,17 +51,17 @@ Q["database.ts"] R["docker-compose.ts"] U["plugin.ts"] Z["elysiajs.ts"] -1B["dockerode.ts"] +1C["dockerode.ts"] end subgraph S["routes"] T["live-logs.ts"] 10["stacks.ts"] -16["utils.ts"] -1G["api-config.ts"] -1H["docker-manager.ts"] -1I["docker-stats.ts"] -1J["docker-websocket.ts"] -1L["logs.ts"] +17["utils.ts"] +1H["api-config.ts"] +1I["docker-manager.ts"] +1J["docker-stats.ts"] +1K["docker-websocket.ts"] +1M["logs.ts"] end subgraph X["middleware"] Y["auth.ts"] @@ -73,24 +74,24 @@ G["bun:sqlite"] subgraph 13["fs"] 14["promises"] end -18["package.json"] -1K["stream"] +19["package.json"] +1L["stream"] 1-->4 1-->W 1-->Y 1-->T 1-->10 -1-->16 +1-->17 1-->Q 1-->D -1-->19 -1-->1E +1-->1A +1-->1F 1-->A -1-->1G 1-->1H 1-->1I 1-->1J -1-->1L +1-->1K +1-->1M 4-->7 4-->D 4-->V @@ -144,63 +145,66 @@ Y-->Z 10-->D 10-->12 10-->A -10-->15 +10-->16 +12-->15 12-->D 12-->A 12-->Q 12-->R -12-->5 12-->14 15-->A -15-->Z -16-->17 -16-->15 +16-->A +16-->Z 17-->18 -19-->D -19-->1A -19-->1C -19-->A -19-->Q +17-->16 +18-->19 1A-->D -1A-->V -1A-->A -1A-->L 1A-->1B -1C-->A -1C-->D -1C-->V -1C-->1D -1E-->1F -1E-->A -1E-->7 -1E-->13 -1E-->B +1A-->1D +1A-->A +1A-->Q +1B-->D +1B-->V +1B-->15 +1B-->A +1B-->L +1B-->1C +1D-->A +1D-->D +1D-->V +1D-->1E +1F-->1G 1F-->A -1F-->14 -1G-->D -1G-->7 +1F-->7 +1F-->13 +1F-->B 1G-->A -1G-->17 -1G-->15 -1G-->Y -1G-->Q +1G-->14 1H-->D +1H-->7 1H-->A -1H-->15 -1H-->L +1H-->18 +1H-->16 +1H-->Y +1H-->Q 1I-->D -1I-->V -1I-->1D 1I-->A -1I-->15 +1I-->16 1I-->L -1I-->1B 1J-->D 1J-->V -1J-->1D -1J-->A +1J-->1E 1J-->15 -1J-->1K -1L-->D -1L-->A +1J-->A +1J-->16 +1J-->L +1J-->1C +1K-->D +1K-->V +1K-->1E +1K-->A +1K-->16 +1K-->1L +1M-->D +1M-->A diff --git a/dependency-graph.svg b/dependency-graph.svg index 66611d2..0549c66 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -4,72 +4,72 @@ - - + + dependency-cruiser output - + cluster_fs - -fs + +fs cluster_src - -src + +src cluster_src/core - -core + +core cluster_src/core/database - -database + +database cluster_src/core/docker - -docker + +docker cluster_src/core/plugins - -plugins + +plugins cluster_src/core/stacks - -stacks + +stacks cluster_src/core/utils - -utils + +utils cluster_src/middleware - -middleware + +middleware cluster_src/routes - -routes + +routes cluster_src/typings - -typings + +typings bun - -bun + +bun @@ -77,8 +77,8 @@ bun:sqlite - -bun:sqlite + +bun:sqlite @@ -86,8 +86,8 @@ events - -events + +events @@ -95,8 +95,8 @@ fs - -fs + +fs @@ -104,8 +104,8 @@ fs/promises - -promises + +promises @@ -113,8 +113,8 @@ package.json - -package.json + +package.json @@ -122,8 +122,8 @@ path - -path + +path @@ -131,8 +131,8 @@ src/core/database/config.ts - -config.ts + +config.ts @@ -140,1155 +140,1182 @@ src/core/database/database.ts - -database.ts + +database.ts src/core/database/config.ts->src/core/database/database.ts - - + + src/core/database/helper.ts - -helper.ts + +helper.ts src/core/database/config.ts->src/core/database/helper.ts - - - - + + + + src/core/database/database.ts->bun:sqlite - - + + src/core/utils/logger.ts - -logger.ts + +logger.ts src/core/database/helper.ts->src/core/utils/logger.ts - - - - + + + + src/core/database/containerStats.ts - -containerStats.ts + +containerStats.ts src/core/database/containerStats.ts->src/core/database/database.ts - - + + src/core/database/containerStats.ts->src/core/database/helper.ts - - - - + + + + src/core/database/dockerHosts.ts - -dockerHosts.ts + +dockerHosts.ts src/core/database/dockerHosts.ts->src/core/database/database.ts - - + + src/core/database/dockerHosts.ts->src/core/database/helper.ts - - - - + + + + src/typings/docker.ts - -docker.ts + +docker.ts src/core/database/dockerHosts.ts->src/typings/docker.ts - - + + - + src/core/utils/logger.ts->path - - + + src/core/database/index.ts - -index.ts + +index.ts - + src/core/utils/logger.ts->src/core/database/index.ts - - - - + + + + src/typings/websocket.ts - -websocket.ts + +websocket.ts - + src/core/utils/logger.ts->src/typings/websocket.ts - - + + - + src/routes/live-logs.ts - - -live-logs.ts + + +live-logs.ts - + src/core/utils/logger.ts->src/routes/live-logs.ts - - - - + + + + src/core/database/hostStats.ts - -hostStats.ts + +hostStats.ts src/core/database/hostStats.ts->src/core/database/database.ts - - + + src/core/database/hostStats.ts->src/core/database/helper.ts - - - - + + + + src/core/database/hostStats.ts->src/typings/docker.ts - - + + src/core/database/index.ts->src/core/database/config.ts - - - - + + + + src/core/database/index.ts->src/core/database/database.ts - - + + src/core/database/index.ts->src/core/database/containerStats.ts - - - - + + + + src/core/database/index.ts->src/core/database/dockerHosts.ts - - - - + + + + src/core/database/index.ts->src/core/database/hostStats.ts - - - - + + + + src/core/database/logs.ts - -logs.ts + +logs.ts src/core/database/index.ts->src/core/database/logs.ts - - - - + + + + src/core/database/stacks.ts - -stacks.ts + +stacks.ts src/core/database/index.ts->src/core/database/stacks.ts - - - - + + + + src/core/database/logs.ts->src/core/database/database.ts - - + + src/core/database/logs.ts->src/core/database/helper.ts - - - - + + + + src/core/database/logs.ts->src/typings/websocket.ts - - + + src/core/database/stacks.ts->src/core/database/database.ts - - + + src/core/database/stacks.ts->src/core/database/helper.ts - - - - + + + + src/typings/database.ts - -database.ts + +database.ts src/core/database/stacks.ts->src/typings/database.ts - - + + src/typings/docker-compose.ts - -docker-compose.ts + +docker-compose.ts src/core/database/stacks.ts->src/typings/docker-compose.ts - - + + src/core/docker/client.ts - -client.ts + +client.ts src/core/docker/client.ts->src/typings/docker.ts - - + + src/core/docker/client.ts->src/core/utils/logger.ts - - + + src/core/docker/monitor.ts - -monitor.ts + +monitor.ts src/core/docker/monitor.ts->bun - - + + src/core/docker/monitor.ts->src/typings/docker.ts - - + + src/core/docker/monitor.ts->src/core/utils/logger.ts - - + + src/core/docker/monitor.ts->src/core/database/index.ts - - + + src/core/docker/monitor.ts->src/core/docker/client.ts - - + + src/core/plugins/plugin-manager.ts - -plugin-manager.ts + +plugin-manager.ts src/core/docker/monitor.ts->src/core/plugins/plugin-manager.ts - - + + - + src/core/plugins/plugin-manager.ts->events - - + + - + src/core/plugins/plugin-manager.ts->src/typings/docker.ts - - + + - + src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts - - + + - + src/typings/plugin.ts - - -plugin.ts + + +plugin.ts - + src/core/plugins/plugin-manager.ts->src/typings/plugin.ts - - + + src/core/docker/scheduler.ts - -scheduler.ts + +scheduler.ts src/core/docker/scheduler.ts->src/core/utils/logger.ts - - + + src/core/docker/scheduler.ts->src/core/database/index.ts - - + + src/core/docker/scheduler.ts->src/typings/database.ts - - + + src/core/docker/store-host-stats.ts - -store-host-stats.ts + +store-host-stats.ts src/core/docker/scheduler.ts->src/core/docker/store-host-stats.ts - - + + src/core/docker/store-container-stats.ts - -store-container-stats.ts + +store-container-stats.ts src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts - - + + - + src/core/docker/store-host-stats.ts->src/typings/docker.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/utils/logger.ts - - + + src/core/docker/store-host-stats.ts->src/core/database/index.ts - - + + src/core/docker/store-host-stats.ts->src/core/docker/client.ts - - + + - + +src/core/utils/helpers.ts + + +helpers.ts + + + + + +src/core/docker/store-host-stats.ts->src/core/utils/helpers.ts + + + + + src/typings/dockerode.ts - - -dockerode.ts + + +dockerode.ts - + src/core/docker/store-host-stats.ts->src/typings/dockerode.ts - - + + src/core/docker/store-container-stats.ts->src/core/utils/logger.ts - - + + src/core/docker/store-container-stats.ts->src/core/database/index.ts - - + + src/core/docker/store-container-stats.ts->src/core/docker/client.ts - - + + src/core/utils/calculations.ts - -calculations.ts + +calculations.ts src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts - - + + + + + +src/core/utils/helpers.ts->src/core/utils/logger.ts + + - + src/core/plugins/loader.ts - - -loader.ts + + +loader.ts - + src/core/plugins/loader.ts->fs - - + + - + src/core/plugins/loader.ts->path - - + + - + src/core/plugins/loader.ts->src/core/utils/logger.ts - - + + - + src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts - - + + - + src/core/utils/change-me-checker.ts - - -change-me-checker.ts + + +change-me-checker.ts - + src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts - - + + - + src/core/utils/change-me-checker.ts->fs/promises - - + + - + src/core/utils/change-me-checker.ts->src/core/utils/logger.ts - - + + - + src/typings/plugin.ts->src/typings/docker.ts - - + + - + src/core/stacks/controller.ts - - -controller.ts + + +controller.ts - - -src/core/stacks/controller.ts->bun - - - - + src/core/stacks/controller.ts->fs/promises - - + + - + src/core/stacks/controller.ts->src/core/utils/logger.ts - - + + - + src/core/stacks/controller.ts->src/core/database/index.ts - - + + - + src/core/stacks/controller.ts->src/typings/database.ts - - + + - + src/core/stacks/controller.ts->src/typings/docker-compose.ts - - + + + + + +src/core/stacks/controller.ts->src/core/utils/helpers.ts + + - + src/routes/live-logs.ts->src/core/utils/logger.ts - - - - + + + + - + src/routes/live-logs.ts->src/typings/websocket.ts - - + + - + src/core/utils/package-json.ts - + package-json.ts - + src/core/utils/package-json.ts->package.json - - + + - + src/core/utils/response-handler.ts - - -response-handler.ts + + +response-handler.ts - + src/core/utils/response-handler.ts->src/core/utils/logger.ts - - + + - + src/typings/elysiajs.ts - - -elysiajs.ts + + +elysiajs.ts - + src/core/utils/response-handler.ts->src/typings/elysiajs.ts - - + + - + src/core/utils/swagger-readme.ts - - -swagger-readme.ts + + +swagger-readme.ts - + src/index.ts - - -index.ts + + +index.ts - + src/index.ts->src/core/utils/logger.ts - - + + - + src/index.ts->src/core/database/index.ts - - + + - + src/index.ts->src/typings/database.ts - - + + - + src/index.ts->src/core/docker/monitor.ts - - + + - + src/index.ts->src/core/docker/scheduler.ts - - + + - + src/index.ts->src/core/plugins/loader.ts - - + + - + src/index.ts->src/routes/live-logs.ts - - + + - + src/index.ts->src/core/utils/swagger-readme.ts - - + + - + src/middleware/auth.ts - - -auth.ts + + +auth.ts - + src/index.ts->src/middleware/auth.ts - - + + - + src/routes/stacks.ts - - -stacks.ts + + +stacks.ts - + src/index.ts->src/routes/stacks.ts - - + + - + src/routes/utils.ts - - -utils.ts + + +utils.ts - + src/index.ts->src/routes/utils.ts - - + + - + src/routes/api-config.ts - - -api-config.ts + + +api-config.ts - + src/index.ts->src/routes/api-config.ts - - + + - + src/routes/docker-manager.ts - - -docker-manager.ts + + +docker-manager.ts - + src/index.ts->src/routes/docker-manager.ts - - + + - + src/routes/docker-stats.ts - - -docker-stats.ts + + +docker-stats.ts - + src/index.ts->src/routes/docker-stats.ts - - + + - + src/routes/docker-websocket.ts - + docker-websocket.ts - + src/index.ts->src/routes/docker-websocket.ts - - + + - + src/routes/logs.ts - - -logs.ts + + +logs.ts - + src/index.ts->src/routes/logs.ts - - + + - + src/middleware/auth.ts->src/core/utils/logger.ts - - + + - + src/middleware/auth.ts->src/core/database/index.ts - - + + - + src/middleware/auth.ts->src/typings/database.ts - - + + - + src/middleware/auth.ts->src/typings/elysiajs.ts - - + + - + src/routes/stacks.ts->src/core/utils/logger.ts - - + + - + src/routes/stacks.ts->src/core/database/index.ts - - + + - + src/routes/stacks.ts->src/core/stacks/controller.ts - - + + - + src/routes/stacks.ts->src/core/utils/response-handler.ts - - + + - + src/routes/utils.ts->src/core/utils/package-json.ts - - + + - + src/routes/utils.ts->src/core/utils/response-handler.ts - - + + - + src/routes/api-config.ts->src/core/utils/logger.ts - - + + - + src/routes/api-config.ts->src/core/database/index.ts - - + + - + src/routes/api-config.ts->src/typings/database.ts - - + + - + src/routes/api-config.ts->src/core/plugins/plugin-manager.ts - - + + - + src/routes/api-config.ts->src/core/utils/package-json.ts - - + + - + src/routes/api-config.ts->src/core/utils/response-handler.ts - - + + - + src/routes/api-config.ts->src/middleware/auth.ts - - + + - + src/routes/docker-manager.ts->src/typings/docker.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-manager.ts->src/core/database/index.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/response-handler.ts - - + + - + src/routes/docker-stats.ts->src/typings/docker.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-stats.ts->src/core/database/index.ts - - + + - + src/routes/docker-stats.ts->src/core/docker/client.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/calculations.ts - - + + + + + +src/routes/docker-stats.ts->src/core/utils/helpers.ts + + - + src/routes/docker-stats.ts->src/typings/dockerode.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/response-handler.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-websocket.ts->src/core/database/index.ts - - + + - + src/routes/docker-websocket.ts->src/core/docker/client.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/response-handler.ts - - + + - + stream - + stream - + src/routes/docker-websocket.ts->stream - + src/routes/logs.ts->src/core/utils/logger.ts - - + + - + src/routes/logs.ts->src/core/database/index.ts - - + + From b3192ec9f324af37dd492c82032ba7f73bd51ee9 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sat, 5 Apr 2025 13:07:15 +0200 Subject: [PATCH 236/324] Feat: More info in the non websocket request --- src/routes/docker-stats.ts | 26 ++++++++++++++------------ src/routes/docker-websocket.ts | 14 +++++++------- src/typings/docker.ts | 5 +++++ 3 files changed, 26 insertions(+), 19 deletions(-) diff --git a/src/routes/docker-stats.ts b/src/routes/docker-stats.ts index b6bdd3f..9cfe465 100644 --- a/src/routes/docker-stats.ts +++ b/src/routes/docker-stats.ts @@ -30,7 +30,7 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) return responseHandler.error( set, pingError as string, - "Docker host connection failed", + "Docker host connection failed" ); } @@ -48,19 +48,19 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) set, reject, "An error occurred", - error, + error ); } if (!stats) { return responseHandler.reject( set, reject, - "No stats available", + "No stats available" ); } resolve(stats); }); - }, + } ); containers.push({ @@ -72,20 +72,22 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) state: containerInfo.State, cpuUsage: calculateCpuPercent(stats), memoryUsage: calculateMemoryUsage(stats), + stats: stats, + info: containerInfo, }); } catch (containerError) { logger.error( "Error fetching container stats,", - containerError, + containerError ); } - }), + }) ); logger.debug(`Fetched stats for ${host.name}`); } catch (hostError) { logger.error("Error fetching containers for host,", hostError); } - }), + }) ); set.headers["Content-Type"] = "application/json"; @@ -95,7 +97,7 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) return responseHandler.error( set, error as string, - "Failed to retrieve containers", + "Failed to retrieve containers" ); } }, @@ -105,7 +107,7 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) description: "Collects real-time statistics for all Docker containers across monitored hosts, including CPU and memory utilization", }, - }, + } ) .get( @@ -117,7 +119,7 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) if (!host) { return responseHandler.simple_error( set, - `Host (${params.id}) not found`, + `Host (${params.id}) not found` ); } @@ -148,7 +150,7 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) return responseHandler.error( set, error as string, - "Failed to retrieve host config", + "Failed to retrieve host config" ); } }, @@ -158,5 +160,5 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) description: "Provides detailed system metrics and Docker runtime information for specified host", }, - }, + } ); diff --git a/src/routes/docker-websocket.ts b/src/routes/docker-websocket.ts index 43292e2..8b26542 100644 --- a/src/routes/docker-websocket.ts +++ b/src/routes/docker-websocket.ts @@ -40,7 +40,7 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( await docker.ping(); const containers = await docker.listContainers({ all: true }); logger.debug( - `Found ${containers.length} containers on ${host.name} (id: ${host.id})`, + `Found ${containers.length} containers on ${host.name} (id: ${host.id})` ); for (const containerInfo of containers) { @@ -75,7 +75,7 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( state: containerInfo.State, cpuUsage: calculateCpuPercent(stats) || 0, memoryUsage: calculateMemoryUsage(stats) || 0, - }), + }) ); } catch (error) { logger.error(`Parse error: ${error}`); @@ -89,7 +89,7 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( hostId: host.name, containerId: containerInfo.Id, error: `Stats stream error: ${error}`, - }), + }) ); }); } @@ -102,9 +102,9 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( { headers: {} }, error as string, "Docker connection failed", - 500, - ), - ), + 500 + ) + ) ); } }, @@ -129,5 +129,5 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( }); connectionStreams.delete(ws); }, - }, + } ); diff --git a/src/typings/docker.ts b/src/typings/docker.ts index 8b4f503..8f8844b 100644 --- a/src/typings/docker.ts +++ b/src/typings/docker.ts @@ -1,3 +1,6 @@ +import { ContainerStats } from "dockerode"; +import Docker from "dockerode"; + interface DockerHost { name: string; hostAddress: string; @@ -14,6 +17,8 @@ interface ContainerInfo { state: string; cpuUsage: number; memoryUsage: number; + stats: ContainerStats; + info: Docker.ContainerInfo; } interface HostStats { From 5a72c1ce56b624ba7fcacaf6ddd557c8b312b0bd Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 15 Apr 2025 19:45:06 +0200 Subject: [PATCH 237/324] Feat: Stacks progress websocket route --- .gitignore | 1 + .local-tests/stacks.md | 1 + bun.lock | 84 +++----- package.json | 8 +- src/core/database/stacks.ts | 17 +- src/core/docker/scheduler.ts | 12 +- src/core/stacks/controller.ts | 292 ++++++++++++++++++---------- src/core/utils/calculations.ts | 2 +- src/core/utils/change-me-checker.ts | 4 +- src/core/utils/helpers.ts | 8 +- src/core/utils/logger.ts | 107 ++++------ src/core/utils/package-json.ts | 9 +- src/core/utils/response-handler.ts | 3 +- src/index.ts | 53 +++-- src/middleware/auth.ts | 11 +- src/plugins/example.plugin.ts | 29 +-- src/plugins/telegram.plugin.ts | 5 +- src/routes/api-config.ts | 27 +-- src/routes/docker-manager.ts | 20 +- src/routes/docker-stats.ts | 10 +- src/routes/docker-websocket.ts | 9 +- src/routes/live-logs.ts | 6 +- src/routes/live-stacks.ts | 30 +++ src/routes/logs.ts | 11 +- src/routes/stacks.ts | 51 ++--- src/routes/utils.ts | 7 +- src/tests/cleanup.ts | 3 +- src/tests/delete.spec.ts | 1 + src/tests/gets.spec.ts | 4 +- src/tests/helper.ts | 4 +- src/tests/post.spec.ts | 4 +- src/typings/database.ts | 10 +- src/typings/plugin.ts | 1 - src/typings/websocket.ts | 24 ++- tsconfig.json | 2 +- 35 files changed, 487 insertions(+), 383 deletions(-) create mode 100644 src/routes/live-stacks.ts diff --git a/.gitignore b/.gitignore index c7aba1f..322656b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /node_modules .test dependency-graph* +build \ No newline at end of file diff --git a/.local-tests/stacks.md b/.local-tests/stacks.md index 4d9290c..22a2c51 100644 --- a/.local-tests/stacks.md +++ b/.local-tests/stacks.md @@ -14,6 +14,7 @@ - stack_prefix ### JSON + ```json { "compose_spec": { diff --git a/bun.lock b/bun.lock index ca73cdb..ffa2cdf 100644 --- a/bun.lock +++ b/bun.lock @@ -9,7 +9,7 @@ "@elysiajs/swagger": "^1.2.2", "chalk": "^5.4.1", "docker-compose": "^1.2.0", - "dockerode": "^4.0.4", + "dockerode": "^4.0.5", "elysia": "latest", "knip": "latest", "split2": "^4.2.0", @@ -17,13 +17,13 @@ "yaml": "^2.7.1", }, "devDependencies": { - "@types/dockerode": "^3.3.36", - "@types/node": "^22.13.14", + "@types/dockerode": "^3.3.38", + "@types/node": "^22.14.1", "@types/split2": "^4.2.3", "bun-types": "latest", "cross-env": "^7.0.3", "logform": "^2.7.0", - "typescript": "^5.8.2", + "typescript": "^5.8.3", "wrap-ansi": "^9.0.0", }, }, @@ -44,17 +44,17 @@ "@elysiajs/swagger": ["@elysiajs/swagger@1.2.2", "", { "dependencies": { "@scalar/themes": "^0.9.52", "@scalar/types": "^0.0.12", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.2.0" } }, "sha512-DG0PbX/wzQNQ6kIpFFPCvmkkWTIbNWDS7lVLv3Puy6ONklF14B4NnbDfpYjX1hdSYKeCqKBBOuenh6jKm8tbYA=="], - "@grpc/grpc-js": ["@grpc/grpc-js@1.13.0", "", { "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-pMuxInZjUnUkgMT2QLZclRqwk2ykJbIU05aZgPgJYXEpN9+2I7z7aNwcjWZSycRPl232FfhPszyBFJyOxTHNog=="], + "@grpc/grpc-js": ["@grpc/grpc-js@1.13.3", "", { "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-FTXHdOoPbZrBjlVLHuKbDZnsTxXv2BlHF57xw6LuThXacXvtkahEPED0CKMk6obZDf65Hv4k3z62eyPNpvinIg=="], "@grpc/proto-loader": ["@grpc/proto-loader@0.7.13", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.2.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw=="], "@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="], - "@nodelib/fs.scandir": ["@nodelib/fs.scandir@4.0.1", "", { "dependencies": { "@nodelib/fs.stat": "4.0.0", "run-parallel": "^1.2.0" } }, "sha512-vAkI715yhnmiPupY+dq+xenu5Tdf2TBQ66jLvBIcCddtz+5Q8LbMKaf9CIJJreez8fQ8fgaY+RaywQx8RJIWpw=="], + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], - "@nodelib/fs.walk": ["@nodelib/fs.walk@3.0.1", "", { "dependencies": { "@nodelib/fs.scandir": "4.0.1", "fastq": "^1.15.0" } }, "sha512-nIh/M6Kh3ZtOmlY00DaUYB4xeeV6F3/ts1l29iwl3/cfyY/OuCfUx+v08zgx8TKPTifXRcjjqVQ4KB2zOYSbyw=="], + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], @@ -78,32 +78,28 @@ "@scalar/openapi-types": ["@scalar/openapi-types@0.1.1", "", {}, "sha512-NMy3QNk6ytcCoPUGJH0t4NNr36OWXgZhA3ormr3TvhX1NDgoF95wFyodGVH8xiHeUyn2/FxtETm8UBLbB5xEmg=="], - "@scalar/themes": ["@scalar/themes@0.9.80", "", { "dependencies": { "@scalar/types": "0.1.2" } }, "sha512-UZM8pQLpGeBtOdUx6yOcj5SPiWo1LaylUVt8HjCRFQ90zZtwbcIWfUWwWOay5nh7cwSVqY2G9eAyGYcNJB12ew=="], + "@scalar/themes": ["@scalar/themes@0.9.86", "", { "dependencies": { "@scalar/types": "0.1.7" } }, "sha512-QUHo9g5oSWi+0Lm1vJY9TaMZRau8LHg+vte7q5BVTBnu6NuQfigCaN+ouQ73FqIVd96TwMO6Db+dilK1B+9row=="], "@scalar/types": ["@scalar/types@0.0.12", "", { "dependencies": { "@scalar/openapi-types": "0.1.1", "@unhead/schema": "^1.9.5" } }, "sha512-XYZ36lSEx87i4gDqopQlGCOkdIITHHEvgkuJFrXFATQs9zHARop0PN0g4RZYWj+ZpCUclOcaOjbCt8JGe22mnQ=="], - "@sinclair/typebox": ["@sinclair/typebox@0.34.30", "", {}, "sha512-gFB3BiqjDxEoadW0zn+xyMVb7cLxPCoblVn2C/BKpI41WPYi2d6fwHAlynPNZ5O/Q4WEiujdnJzVtvG/Jc2CBQ=="], - - "@snyk/github-codeowners": ["@snyk/github-codeowners@1.1.0", "", { "dependencies": { "commander": "^4.1.1", "ignore": "^5.1.8", "p-map": "^4.0.0" }, "bin": { "github-codeowners": "dist/cli.js" } }, "sha512-lGFf08pbkEac0NYgVf4hdANpAgApRjNByLXB+WBip3qj1iendOIyAwP2GKkKbQMNVy2r1xxDf0ssfWscoiC+Vw=="], + "@sinclair/typebox": ["@sinclair/typebox@0.34.33", "", {}, "sha512-5HAV9exOMcXRUxo+9iYB5n09XxzCXnfy4VTNW4xnDv+FgjzAGY989C28BIdljKqmF+ZltUwujE3aossvcVtq6g=="], "@types/docker-modem": ["@types/docker-modem@3.0.6", "", { "dependencies": { "@types/node": "*", "@types/ssh2": "*" } }, "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg=="], - "@types/dockerode": ["@types/dockerode@3.3.36", "", { "dependencies": { "@types/docker-modem": "*", "@types/node": "*", "@types/ssh2": "*" } }, "sha512-K0wTBKjjVI1xS4zeLynssmmbpPl4AnWZ/MJ3JBTi9eGzEmu+xgMLVSKiWzsy/z+3GBPLD5+uE/i/6ZTeZPaX7A=="], + "@types/dockerode": ["@types/dockerode@3.3.38", "", { "dependencies": { "@types/docker-modem": "*", "@types/node": "*", "@types/ssh2": "*" } }, "sha512-nnrcfUe2iR+RyOuz0B4bZgQwD9djQa9ADEjp7OAgBs10pYT0KSCtplJjcmBDJz0qaReX5T7GbE5i4VplvzUHvA=="], - "@types/node": ["@types/node@22.13.14", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w=="], + "@types/node": ["@types/node@22.14.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw=="], "@types/split2": ["@types/split2@4.2.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-59OXIlfUsi2k++H6CHgUQKEb2HKRokUA39HY1i1dS8/AIcqVjtAAFdf8u+HxTWK/4FUHMJQlKSZ4I6irCBJ1Zw=="], - "@types/ssh2": ["@types/ssh2@1.15.4", "", { "dependencies": { "@types/node": "^18.11.18" } }, "sha512-9JTQgVBWSgq6mAen6PVnrAmty1lqgCMvpfN+1Ck5WRUsyMYPa6qd50/vMJ0y1zkGpOEgLzm8m8Dx/Y5vRouLaA=="], + "@types/ssh2": ["@types/ssh2@1.15.5", "", { "dependencies": { "@types/node": "^18.11.18" } }, "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ=="], "@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="], - "@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="], + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], "@unhead/schema": ["@unhead/schema@1.11.20", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA=="], - "aggregate-error": ["aggregate-error@3.1.0", "", { "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" } }, "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA=="], - "ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], "ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], @@ -126,14 +122,12 @@ "buildcheck": ["buildcheck@0.0.6", "", {}, "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A=="], - "bun-types": ["bun-types@1.2.7", "", { "dependencies": { "@types/node": "*", "@types/ws": "*" } }, "sha512-P4hHhk7kjF99acXqKvltyuMQ2kf/rzIw3ylEDpCxDS9Xa0X0Yp/gJu/vDCucmWpiur5qJ0lwB2bWzOXa2GlHqA=="], + "bun-types": ["bun-types@1.2.9", "", { "dependencies": { "@types/node": "*", "@types/ws": "*" } }, "sha512-dk/kOEfQbajENN/D6FyiSgOKEuUi9PWfqKQJEgwKrCMWbjS/S6tEXp178mWvWAcUSYm9ArDlWHZKO3T/4cLXiw=="], "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], - "clean-stack": ["clean-stack@2.2.0", "", {}, "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="], - "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], "clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="], @@ -148,8 +142,6 @@ "colorspace": ["colorspace@1.1.4", "", { "dependencies": { "color": "^3.1.3", "text-hex": "1.0.x" } }, "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w=="], - "commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], - "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], "cpu-features": ["cpu-features@0.0.10", "", { "dependencies": { "buildcheck": "~0.0.6", "nan": "^2.19.0" } }, "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA=="], @@ -166,7 +158,7 @@ "docker-modem": ["docker-modem@5.0.6", "", { "dependencies": { "debug": "^4.1.1", "readable-stream": "^3.5.0", "split-ca": "^1.0.1", "ssh2": "^1.15.0" } }, "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ=="], - "dockerode": ["dockerode@4.0.4", "", { "dependencies": { "@balena/dockerignore": "^1.0.2", "@grpc/grpc-js": "^1.11.1", "@grpc/proto-loader": "^0.7.13", "docker-modem": "^5.0.6", "protobufjs": "^7.3.2", "tar-fs": "~2.0.1", "uuid": "^10.0.0" } }, "sha512-6GYP/EdzEY50HaOxTVTJ2p+mB5xDHTMJhS+UoGrVyS6VC+iQRh7kZ4FRpUYq6nziby7hPqWhOrFFUFTMUZJJ5w=="], + "dockerode": ["dockerode@4.0.5", "", { "dependencies": { "@balena/dockerignore": "^1.0.2", "@grpc/grpc-js": "^1.11.1", "@grpc/proto-loader": "^0.7.13", "docker-modem": "^5.0.6", "protobufjs": "^7.3.2", "tar-fs": "~2.1.2", "uuid": "^10.0.0" } }, "sha512-ZPmKSr1k1571Mrh7oIBS/j0AqAccoecY2yH420ni5j1KyNMgnoTh4Nu4FWunh0HZIJmRSmSysJjBIpa/zyWUEA=="], "easy-table": ["easy-table@1.2.0", "", { "dependencies": { "ansi-regex": "^5.0.1" }, "optionalDependencies": { "wcwidth": "^1.0.1" } }, "sha512-OFzVOv03YpvtcWGe5AayU5G2hgybsg3iqA6drU8UaoZyB9jLGMTrz9+asnLp/E+6qPh88yEI1gvyZFZ41dmgww=="], @@ -206,10 +198,6 @@ "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], - - "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], @@ -230,7 +218,7 @@ "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], - "knip": ["knip@5.46.3", "", { "dependencies": { "@nodelib/fs.walk": "3.0.1", "@snyk/github-codeowners": "1.1.0", "easy-table": "1.2.0", "enhanced-resolve": "^5.18.0", "fast-glob": "^3.3.3", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "minimist": "^1.2.8", "picocolors": "^1.1.0", "picomatch": "^4.0.1", "pretty-ms": "^9.0.0", "smol-toml": "^1.3.1", "strip-json-comments": "5.0.1", "summary": "2.1.0", "zod": "^3.22.4", "zod-validation-error": "^3.0.3" }, "peerDependencies": { "@types/node": ">=18", "typescript": ">=5.0.4" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-DpxZYvFDh0POjgnfXie39zd4SCxmw3iQTSLPgnf1Umq+k+sCHjcv553UmI3hfo39qlVIq2c8XSsjS3IeZfdAoA=="], + "knip": ["knip@5.50.4", "", { "dependencies": { "@nodelib/fs.walk": "^1.2.3", "easy-table": "1.2.0", "enhanced-resolve": "^5.18.1", "fast-glob": "^3.3.3", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "minimist": "^1.2.8", "picocolors": "^1.1.0", "picomatch": "^4.0.1", "pretty-ms": "^9.0.0", "smol-toml": "^1.3.1", "strip-json-comments": "5.0.1", "zod": "^3.22.4", "zod-validation-error": "^3.0.3" }, "peerDependencies": { "@types/node": ">=18", "typescript": ">=5.0.4" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-In+GjPpd2P3IDZnBBP4QF27vhQOhuBkICiuN9j+DMOf/m/qAFLGcbvuAGxco8IDvf26pvBnfeSmm1f6iNCkgOA=="], "kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="], @@ -254,6 +242,8 @@ "nan": ["nan@2.22.2", "", {}, "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ=="], + "nanoid": ["nanoid@5.1.5", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw=="], + "node-cache": ["node-cache@5.1.2", "", { "dependencies": { "clone": "2.x" } }, "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg=="], "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], @@ -262,8 +252,6 @@ "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], - "p-map": ["p-map@4.0.0", "", { "dependencies": { "aggregate-error": "^3.0.0" } }, "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ=="], - "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], @@ -276,7 +264,7 @@ "pretty-ms": ["pretty-ms@9.2.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg=="], - "protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], + "protobufjs": ["protobufjs@7.5.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-Z2E/kOY1QjoMlCytmexzYfDm/w5fKAiRwpSzGtdnXW1zC88Z2yXazHHrOtwCzn+7wSxyE8PYM4rvVcMphF9sOA=="], "pump": ["pump@3.0.2", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw=="], @@ -320,11 +308,9 @@ "strip-json-comments": ["strip-json-comments@5.0.1", "", {}, "sha512-0fk9zBqO67Nq5M/m45qHCJxylV/DhBlIOVExqgOMiCCrzrhU6tCibRXNqE3jwJLftzE9SNuZtYbpzcO+i9FiKw=="], - "summary": ["summary@2.1.0", "", {}, "sha512-nMIjMrd5Z2nuB2RZCKJfFMjgS3fygbeyGk9PxPPaJR1RIcyN9yn4A63Isovzm3ZtQuEkLBVgMdPup8UeLH7aQw=="], - "tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="], - "tar-fs": ["tar-fs@2.0.1", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.0.0" } }, "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA=="], + "tar-fs": ["tar-fs@2.1.2", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA=="], "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], @@ -336,9 +322,11 @@ "tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="], - "typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], + "type-fest": ["type-fest@4.40.0", "", {}, "sha512-ABHZ2/tS2JkvH1PEjxFDTUWC8dB5OsIGZP4IFLhR293GqT5Y5qB1WwL2kMPYhQW9DVgVD8Hd7I8gjwPIf5GFkw=="], + + "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], - "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], @@ -370,17 +358,9 @@ "zod-validation-error": ["zod-validation-error@3.4.0", "", { "peerDependencies": { "zod": "^3.18.0" } }, "sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ=="], - "@nodelib/fs.scandir/@nodelib/fs.stat": ["@nodelib/fs.stat@4.0.0", "", {}, "sha512-ctr6bByzksKRCV0bavi8WoQevU6plSp2IkllIsEqaiKe2mwNNnaluhnRhcsgGZHrrHk57B3lf95MkLMO3STYcg=="], + "@scalar/themes/@scalar/types": ["@scalar/types@0.1.7", "", { "dependencies": { "@scalar/openapi-types": "0.2.0", "@unhead/schema": "^1.11.11", "nanoid": "^5.1.5", "type-fest": "^4.20.0", "zod": "^3.23.8" } }, "sha512-irIDYzTQG2KLvFbuTI8k2Pz/R4JR+zUUSykVTbEMatkzMmVFnn1VzNSMlODbadycwZunbnL2tA27AXed9URVjw=="], - "@scalar/themes/@scalar/types": ["@scalar/types@0.1.2", "", { "dependencies": { "@scalar/openapi-types": "0.1.9", "@unhead/schema": "^1.11.11", "zod": "^3.23.8" } }, "sha512-5kCLQRwAYWt1ds110EaUb9yonc3KoQYNyo4YUCigJLOnoNugbqkEX0zRudGevItiuk+xg4uOYd30r3C+6xAasA=="], - - "@types/docker-modem/@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="], - - "@types/split2/@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="], - - "@types/ssh2/@types/node": ["@types/node@18.19.80", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-kEWeMwMeIvxYkeg1gTc01awpwLbfMRZXdIhwRcakd/KlK53jmRC26LqcbIt7fnAQTu5GzlnWmzA3H6+l1u6xxQ=="], - - "@types/ws/@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="], + "@types/ssh2/@types/node": ["@types/node@18.19.86", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-fifKayi175wLyKyc5qUfyENhQ1dCNI1UNjp653d8kuYcPQN5JhX3dGuP/XmvPTg/xRBn1VTLpbmi+H/Mr7tLfQ=="], "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -388,23 +368,15 @@ "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - "color-string/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - "defaults/clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], - "docker-compose/yaml": ["yaml@2.7.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA=="], - "easy-table/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "fast-glob/@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "protobufjs/@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="], - "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.1.9", "", {}, "sha512-HQQudOSQBU7ewzfnBW9LhDmBE2XOJgSfwrh5PlUB7zJup/kaRkBGNgV2wMjNz9Af/uztiU/xNrO179FysmUT+g=="], + "@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.2.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA=="], "@types/ssh2/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], @@ -414,8 +386,6 @@ "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "fast-glob/@nodelib/fs.walk/@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], - "yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], diff --git a/package.json b/package.json index 620e688..b29b98d 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "@elysiajs/swagger": "^1.2.2", "chalk": "^5.4.1", "docker-compose": "^1.2.0", - "dockerode": "^4.0.4", + "dockerode": "^4.0.5", "elysia": "latest", "knip": "latest", "split2": "^4.2.0", @@ -35,13 +35,13 @@ "yaml": "^2.7.1" }, "devDependencies": { - "@types/dockerode": "^3.3.36", - "@types/node": "^22.13.14", + "@types/dockerode": "^3.3.38", + "@types/node": "^22.14.1", "@types/split2": "^4.2.3", "bun-types": "latest", "cross-env": "^7.0.3", "logform": "^2.7.0", - "typescript": "^5.8.2", + "typescript": "^5.8.3", "wrap-ansi": "^9.0.0" }, "module": "src/index.js", diff --git a/src/core/database/stacks.ts b/src/core/database/stacks.ts index 0491625..18aa116 100644 --- a/src/core/database/stacks.ts +++ b/src/core/database/stacks.ts @@ -2,6 +2,7 @@ import { Stack } from "~/typings/docker-compose"; import { db } from "./database"; import { executeDbOperation } from "./helper"; import type { stacks_config } from "~/typings/database"; +import { findObjectByKey } from "../utils/helpers"; const stmt = { insert: db.prepare(` @@ -26,7 +27,7 @@ const stmt = { }; export function addStack(stack: stacks_config) { - return executeDbOperation("Add Stack", () => + executeDbOperation("Add Stack", () => stmt.insert.run( stack.name, stack.version, @@ -35,14 +36,16 @@ export function addStack(stack: stacks_config) { stack.container_count, stack.stack_prefix, stack.automatic_reboot_on_error, - stack.image_updates, - ), + stack.image_updates + ) ); + + return findObjectByKey(getStacks(), "name", stack.name)?.id; } export function getStacks() { return executeDbOperation("Get Stacks", () => - stmt.selectAll.all(), + stmt.selectAll.all() ) as Stack[]; } @@ -52,7 +55,7 @@ export function deleteStack(id: number) { () => stmt.delete.run(id), () => { if (typeof id !== "number") throw new TypeError("Invalid stack ID"); - }, + } ); } @@ -66,7 +69,7 @@ export function updateStack(stack: stacks_config) { stack.stack_prefix, stack.automatic_reboot_on_error, stack.image_updates, - stack.name, - ), + stack.name + ) ); } diff --git a/src/core/docker/scheduler.ts b/src/core/docker/scheduler.ts index 2d7fae1..d1dd124 100644 --- a/src/core/docker/scheduler.ts +++ b/src/core/docker/scheduler.ts @@ -2,7 +2,7 @@ import storeContainerData from "~/core/docker/store-container-stats"; import { dbFunctions } from "~/core/database"; import { config } from "~/typings/database"; import { logger } from "~/core/utils/logger"; -import storeHostData from "~/core/docker//store-host-stats"; +import storeHostData from "~/core/docker/store-host-stats"; function convertFromMinToMs(minutes: number): number { return minutes * 60 * 1000; @@ -11,7 +11,7 @@ function convertFromMinToMs(minutes: number): number { async function initialRun( scheduleName: string, scheduleFunction: Promise | void, - isAsync: boolean, + isAsync: boolean ) { try { if (isAsync) { @@ -54,15 +54,15 @@ async function setSchedules() { } logger.info( - `Scheduling: Fetching container statistics every ${fetching_interval} minutes`, + `Scheduling: Fetching container statistics every ${fetching_interval} minutes` ); logger.info( - `Scheduling: Updating host statistics every ${fetching_interval} minutes`, + `Scheduling: Updating host statistics every ${fetching_interval} minutes` ); logger.info( - `Scheduling: Cleaning up Database every hour and deleting data older then ${keep_data_for} days`, + `Scheduling: Cleaning up Database every hour and deleting data older then ${keep_data_for} days` ); // Schedule container data fetching @@ -93,7 +93,7 @@ async function setSchedules() { await initialRun( "dbFunctions.deleteOldData", dbFunctions.deleteOldData(keep_data_for), - false, + false ); setInterval(() => { try { diff --git a/src/core/stacks/controller.ts b/src/core/stacks/controller.ts index 1c85b59..19bd082 100644 --- a/src/core/stacks/controller.ts +++ b/src/core/stacks/controller.ts @@ -6,6 +6,16 @@ import type { Stack, ComposeSpec } from "~/typings/docker-compose"; import type { stacks_config } from "~/typings/database"; import { rm } from "node:fs/promises"; import { findObjectByKey } from "../utils/helpers"; +import { postToClient } from "~/routes/live-stacks"; + +const wrapProgressCallback = (progressCallback?: (log: string) => void) => { + return progressCallback + ? (chunk: Buffer, streamSource?: "stdout" | "stderr") => { + const log = chunk.toString(); + progressCallback(log); + } + : undefined; +}; async function getStackName(stack_id: number): Promise { logger.debug(`Fetching stack name for id ${stack_id}`); @@ -19,16 +29,44 @@ async function getStackName(stack_id: number): Promise { async function runStackCommand( stack_id: number, - command: (cwd: string) => Promise, - action: string, + command: ( + cwd: string, + progressCallback?: (log: string) => void + ) => Promise, + action: string ): Promise { try { - const stack = { id: stack_id, name: await getStackName(stack_id) }; - const stackPath = await getStackPath(stack as Stack); - return await command(stackPath); + const stackName = await getStackName(stack_id); + const stackPath = await getStackPath({ + id: stack_id, + name: stackName, + } as Stack); + + const progressCallback = (log: string) => { + postToClient({ + type: "stack-progress", + data: { + stack_id, + action, + message: log.trim(), + timestamp: new Date().toISOString(), + }, + }); + }; + + return await command(stackPath, progressCallback); } catch (error: any) { + postToClient({ + type: "stack-error", + data: { + stack_id, + action, + message: error.message || String(error), + timestamp: new Date().toISOString(), + }, + }); throw new Error( - `Error while ${action} stack "${stack_id}": ${error.message || error}`, + `Error while ${action} stack "${stack_id}": ${error.message || error}` ); } } @@ -54,21 +92,21 @@ export async function deployStack( automatic_reboot_on_error: boolean, isCustom: boolean, image_updates: boolean, - stack_prefix?: string, + stack_prefix?: string ): Promise { + let stackId: number; + try { logger.debug(`Deploying Stack: ${JSON.stringify(stack)}`); - const serviceCount = stack.services ? Object.keys(stack.services).length : 0; - const resolvedPrefix = stack_prefix ?? ""; const stack_config: stacks_config = { id: 0, - name: name, - version: version, + name, + version, source, stack_prefix: resolvedPrefix, automatic_reboot_on_error, @@ -77,137 +115,183 @@ export async function deployStack( image_updates, }; - if (!stack.name) { - logger.debug(`${JSON.stringify(stack)}`); + if (!name) { throw new Error("Stack name needed"); } - dbFunctions.addStack(stack_config); + stackId = dbFunctions.addStack(stack_config) as number; + postToClient({ + type: "stack-status", + data: { + stack_id: stackId, + status: "pending", + message: "Creating stack configuration", + }, + }); const stackYaml: Stack = { - name: name, - source: source, - version: version, + id: stackId, + name, + source, + version, compose_spec: stack, }; + await createStackYAML(stackYaml); - const stackPath = await getStackPath(stackYaml); - await DockerCompose.upAll({ cwd: stackPath }); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - throw new Error(errorMsg); - } -} -export async function stopStack(stack_id: number): Promise { - try { await runStackCommand( - stack_id, - (cwd) => DockerCompose.downAll({ cwd }), - "stopping", + stackId, + (cwd, progressCallback) => + DockerCompose.upAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "deploying" ); + + postToClient({ + type: "stack-status", + data: { + stack_id: stackId, + status: "deployed", + message: "Stack deployed successfully", + }, + }); } catch (error: unknown) { const errorMsg = error instanceof Error ? error.message : String(error); logger.error(errorMsg); + postToClient({ + type: "stack-error", + data: { + stack_id: 0, + action: "deploying", + message: errorMsg, + timestamp: new Date().toISOString(), + }, + }); throw new Error(errorMsg); } } +export async function stopStack(stack_id: number): Promise { + // Note the await to discard the result (convert to void) + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.downAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "stopping" + ); +} + export async function startStack(stack_id: number): Promise { - try { - await runStackCommand( - stack_id, - (cwd) => DockerCompose.upAll({ cwd }), - "starting", - ); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - throw new Error(errorMsg); - } + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.upAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "starting" + ); } export async function pullStackImages(stack_id: number): Promise { - try { - await runStackCommand( - stack_id, - (cwd) => DockerCompose.pullAll({ cwd }), - "pulling images for", - ); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - throw new Error(errorMsg); - } + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.pullAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "pulling-images" + ); } export async function restartStack(stack_id: number): Promise { - try { - await runStackCommand( - stack_id, - (cwd) => DockerCompose.restartAll({ cwd }), - "restarting", - ); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - throw new Error(errorMsg); - } + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.restartAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "restarting" + ); } -export async function getStackStatus(stack_id: number): Promise { - try { - return await runStackCommand( - stack_id, - async (cwd) => { - const rawStatus = await DockerCompose.ps({ cwd }); - return rawStatus.data.services.reduce((acc: any, service: any) => { - acc[service.name] = service.state; - return acc; - }, {}); - }, - "retrieving status for", - ); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - throw new Error(errorMsg); - } +export async function getStackStatus( + stack_id: number +): Promise> { + // Wrap the returned status value to match Promise if that is the expectation. + // In this case, if you need the status, you might adjust the type signature. + const status = await runStackCommand( + stack_id, + async (cwd) => { + const rawStatus = await DockerCompose.ps({ cwd }); + return rawStatus.data.services.reduce((acc: any, service: any) => { + acc[service.name] = service.state; + return acc; + }, {}); + }, + "status-check" + ); + return status; } export async function removeStack(stack_id: number): Promise { try { await runStackCommand( stack_id, - async (cwd) => { - await DockerCompose.down({ cwd }); + async (cwd, progressCallback) => { + await DockerCompose.down({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }); }, - "removing", + "removing" ); const stackName = await getStackName(stack_id); - - const stack = { + const stackPath = await getStackPath({ + id: stack_id, name: stackName, - }; - - const stackPath = await getStackPath(stack as Stack); + } as Stack); try { await rm(stackPath, { recursive: true }); } catch (error: any) { - if (error.code === "ENOENT") { - console.log("Directory doesn't exist"); - } else { - throw error; - } + if (error.code !== "ENOENT") throw error; } + dbFunctions.deleteStack(stack_id); - logger.info(`Stack ${stackName} (${stack_id}) removed successfully`); + postToClient({ + type: "stack-removed", + data: { + stack_id, + message: "Stack removed successfully", + }, + }); } catch (error: unknown) { const errorMsg = error instanceof Error ? error.message : String(error); logger.error(errorMsg); + postToClient({ + type: "stack-error", + data: { + stack_id, + action: "removing", + message: errorMsg, + timestamp: new Date().toISOString(), + }, + }); throw new Error(errorMsg); } } @@ -227,19 +311,17 @@ export async function getAllStacksStatus(): Promise> { return acc; }, {}); }, - "retrieving status for", + "status-check" ); - return { stackName: stack.name, status }; - }), + return { stackId: stack.id, status }; + }) ); - return statusResults.reduce( - (acc, { stackName, status }) => { - acc[stackName] = status; - return acc; - }, - {} as Record, - ); + return statusResults.reduce((acc, { stackId, status }) => { + // Ensure stackId is used as a string if necessary, e.g. + acc[String(stackId)] = status; + return acc; + }, {} as Record); } catch (error: unknown) { const errorMsg = error instanceof Error ? error.message : String(error); logger.error(errorMsg); diff --git a/src/core/utils/calculations.ts b/src/core/utils/calculations.ts index 3f12f95..60ab40b 100644 --- a/src/core/utils/calculations.ts +++ b/src/core/utils/calculations.ts @@ -30,7 +30,7 @@ const calculateCpuPercent = (stats: Docker.ContainerStats): number => { const calculateMemoryUsage = (stats: Docker.ContainerStats): number => { if (stats == null) { - return 0.0; + return 0; } const data = (stats.memory_stats.usage / stats.memory_stats.limit) * 100; diff --git a/src/core/utils/change-me-checker.ts b/src/core/utils/change-me-checker.ts index fa19520..7486048 100644 --- a/src/core/utils/change-me-checker.ts +++ b/src/core/utils/change-me-checker.ts @@ -11,6 +11,8 @@ export async function checkFileForChangeMe(filePath: string) { } if (regex.test(content)) { - throw new Error(`Error: The file contains 'CHANGE_ME'. Please update it.`); + throw new Error( + `The file contains ${regex.exec(content)}. Please update it.` + ); } } diff --git a/src/core/utils/helpers.ts b/src/core/utils/helpers.ts index 7f3a99c..689e566 100644 --- a/src/core/utils/helpers.ts +++ b/src/core/utils/helpers.ts @@ -3,13 +3,9 @@ import { logger } from "./logger"; export function findObjectByKey( array: T[], key: keyof T, - value: T[keyof T], + value: T[keyof T] ): T | undefined { + logger.debug(`Searching ${String(key)}`); const data = array.find((item) => item[key] === value); - logger.debug( - `Searching ${String(key)} = ${String(value)} in ${String( - JSON.stringify(array), - )} Found Item ${JSON.stringify(data)}`, - ); return data; } diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts index 6c0c5ee..a10dfd2 100644 --- a/src/core/utils/logger.ts +++ b/src/core/utils/logger.ts @@ -1,11 +1,14 @@ -import { createLogger, format, transports } from "winston"; -import type { TransformableInfo } from "logform"; import path from "path"; +import wrapAnsi from "wrap-ansi"; import chalk, { ChalkInstance } from "chalk"; +import type { TransformableInfo } from "logform"; +import { createLogger, format, transports } from "winston"; + import { dbFunctions } from "~/core/database"; -import wrapAnsi from "wrap-ansi"; + import { logToClients } from "~/routes/live-logs"; -import type { logStreamData } from "~/typings/websocket"; + +import { log_message } from "~/typings/database"; const padNewlines = process.env.PAD_NEW_LINES !== "false"; @@ -19,15 +22,6 @@ type LogLevel = | "task" | "ut"; -interface CustomTransformableInfo extends TransformableInfo { - file: string; - line: number; -} - -type LogStreamData = Omit & { - message: string; -}; - const ansiRegex = /\x1B\[[0-?9;]*[mG]/g; const formatTerminalMessage = (message: string, prefix: string): string => { @@ -67,55 +61,30 @@ const levelColors: Record = { ut: chalk.hex("#9D00FF"), }; -const handleWebSocketLog = ( - level: string, - timestamp: string, - message: string, - file: string, - line: number, -) => { +const handleWebSocketLog = (log: log_message) => { try { - const data = { - timestamp, - level: level, - message: message, - file: file, - line: line, - }; - - logToClients(data); + logToClients(log); } catch (error) { console.error( - `WebSocket logging failed: ${error instanceof Error ? error.message : error}`, + `WebSocket logging failed: ${ + error instanceof Error ? error.message : error + }` ); } }; -const handleDatabaseLog = ( - level: string, - timestamp: string, - message: string, - file: string, - line: number, -): void => { +const handleDatabaseLog = (log: log_message): void => { try { - const data = { - timestamp, - level, - message, - file: file, - line: line, - }; - - dbFunctions.addLogEntry(data); + dbFunctions.addLogEntry(log); } catch (error) { console.error( - `Database logging failed: ${error instanceof Error ? error.message : error}`, + `Database logging failed: ${ + error instanceof Error ? error.message : error + }` ); } }; -// Main logger export const logger = createLogger({ level: process.env.LOG_LEVEL || "debug", format: format.combine( @@ -145,7 +114,7 @@ export const logger = createLogger({ })(), format.printf((info) => { const { timestamp, level, message, file, line } = - info as CustomTransformableInfo; + info as TransformableInfo & log_message; let processedLevel = level as LogLevel; let processedMessage = String(message); @@ -166,38 +135,40 @@ export const logger = createLogger({ } if (file.endsWith("plugin.ts")) { - processedMessage = `[ ${chalk.greenBright("Plugin")} ] ${processedMessage}`; + processedMessage = `[ ${chalk.grey(file)} ] ${processedMessage}`; } const paddedLevel = processedLevel.toUpperCase().padEnd(5); const coloredLevel = (levelColors[processedLevel] || chalk.white)( - paddedLevel, + paddedLevel ); const coloredContext = chalk.cyan(`${file}:${line}`); const coloredTimestamp = chalk.yellow(timestamp); const prefix = `${paddedLevel} [ ${timestamp} ] - `; + const combinedContent = `${processedMessage} - ${coloredContext}`; + const formattedMessage = padNewlines - ? formatTerminalMessage(processedMessage, prefix) - : processedMessage; - - handleDatabaseLog( - coloredTimestamp.replace(ansiRegex, "").trim(), - coloredLevel.replace(ansiRegex, "").trim(), - processedMessage.replace(ansiRegex, "").trim(), - file.trim(), + ? formatTerminalMessage(combinedContent, prefix) + : combinedContent; + + handleDatabaseLog({ + level: processedLevel, + timestamp, + message: processedMessage, + file, line, - ); - handleWebSocketLog( - coloredLevel.replace(ansiRegex, "").trim(), - coloredTimestamp.replace(ansiRegex, "").trim(), - processedMessage.replace(ansiRegex, "").trim(), - file.trim(), + }); + handleWebSocketLog({ + level: processedLevel, + timestamp, + message: processedMessage, + file, line, - ); + }); - return `${coloredLevel} [ ${coloredTimestamp} ] - ${formattedMessage} - [ ${coloredContext} ]`; - }), + return `${coloredLevel} [ ${coloredTimestamp} ] - ${formattedMessage}`; + }) ), transports: [new transports.Console()], }); diff --git a/src/core/utils/package-json.ts b/src/core/utils/package-json.ts index 3872d56..9147d2f 100644 --- a/src/core/utils/package-json.ts +++ b/src/core/utils/package-json.ts @@ -1,10 +1,17 @@ import packageJson from "~/../package.json"; -const { version, description, license, contributors, dependencies, devDependencies } = packageJson; + +const { version, description, license, dependencies, devDependencies } = + packageJson; +let { contributors } = packageJson; const authorName = packageJson.author.name; const authorEmail = packageJson.author.email; const authorWebsite = packageJson.author.url; +if ((contributors = [])) { + contributors = [":(" as never]; +} + export { version, description, diff --git a/src/core/utils/response-handler.ts b/src/core/utils/response-handler.ts index 369e917..60a11ea 100644 --- a/src/core/utils/response-handler.ts +++ b/src/core/utils/response-handler.ts @@ -1,4 +1,5 @@ import { logger } from "~/core/utils/logger"; + import type { set } from "~/typings/elysiajs"; export const responseHandler = { @@ -6,7 +7,7 @@ export const responseHandler = { set: set, error: string, response_message: string, - error_code?: number, + error_code?: number ) { set.status = error_code || 500; logger.error(`${response_message} - ${error}`); diff --git a/src/index.ts b/src/index.ts index 16cd5ee..cf5edef 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,23 +1,33 @@ -import { dbFunctions } from "~/core/database"; -import { swagger } from "@elysiajs/swagger"; import { Elysia } from "elysia"; -import { loadPlugins } from "~/core/plugins/loader"; +import staticPlugin from "@elysiajs/static"; +import { swagger } from "@elysiajs/swagger"; +import { serverTiming } from "@elysiajs/server-timing"; + import { logger } from "~/core/utils/logger"; +import { dbFunctions } from "~/core/database"; +import { loadPlugins } from "~/core/plugins/loader"; +import { setSchedules } from "~/core/docker/scheduler"; +import { monitorDockerEvents } from "~/core/docker/monitor"; +import { swaggerReadme } from "~/core/utils/swagger-readme"; +import { + authorWebsite, + contributors, + license, +} from "~/core/utils/package-json"; + +import { validateApiKey } from "~/middleware/auth"; + +import { backendLogs } from "~/routes/logs"; +import { utilRoutes } from "~/routes/utils"; +import { liveLogs } from "~/routes/live-logs"; +import { stackRoutes } from "~/routes/stacks"; +import { apiConfigRoutes } from "~/routes/api-config"; import { dockerRoutes } from "~/routes/docker-manager"; import { dockerStatsRoutes } from "~/routes/docker-stats"; -import { backendLogs } from "~/routes/logs"; import { dockerWebsocketRoutes } from "~/routes/docker-websocket"; -import { stackRoutes } from "./routes/stacks"; -import { apiConfigRoutes } from "~/routes/api-config"; -import { setSchedules } from "~/core/docker/scheduler"; -import { serverTiming } from "@elysiajs/server-timing"; -import staticPlugin from "@elysiajs/static"; -import { config } from "./typings/database"; -import { validateApiKey } from "./middleware/auth"; -import { monitorDockerEvents } from "./core/docker/monitor"; -import { liveLogs } from "./routes/live-logs"; -import { utilRoutes } from "./routes/utils"; -import { swaggerReadme } from "./core/utils/swagger-readme"; +import { liveStacks } from "./routes/live-stacks"; + +import { config } from "~/typings/database"; console.log(""); @@ -69,7 +79,7 @@ export const DockStatAPI = new Elysia() }, ], }, - }), + }) ) .onBeforeHandle(async (context) => { const { path, request, set } = context; @@ -96,6 +106,7 @@ export const DockStatAPI = new Elysia() .use(stackRoutes) .use(utilRoutes) .use(liveLogs) + .use(liveStacks) .get("/health", () => ({ status: "healthy" }), { tags: ["Utils"] }) .onError(({ code, set, path }) => { if (code === "NOT_FOUND") { @@ -129,7 +140,7 @@ async function startServer() { if (apiKey === "changeme") { logger.warn( - "Default API Key of 'changeme' detected. Please change your API Key via the `/config/update` route!", + "Default API Key of 'changeme' detected. Please change your API Key via the `/config/update` route!" ); } @@ -138,11 +149,11 @@ async function startServer() { console.log("----- [ ############## ]"); logger.info(`DockStatAPI is running at http://${hostname}:${port}`); logger.info( - `Swagger API Documentation available at http://${hostname}:${port}/swagger`, - ); - logger.info( - `tRPC Endpoint available at: http://${hostname}:${port}/trpc`, + `Swagger API Documentation available at http://${hostname}:${port}/swagger` ); + logger.info(`License: ${license}`); + logger.info(`Author: ${authorWebsite}`); + logger.info(`Contributors: ${contributors}`); }); } catch (error) { logger.error("Failed to start server:", error); diff --git a/src/middleware/auth.ts b/src/middleware/auth.ts index 17ae2c6..48aad39 100644 --- a/src/middleware/auth.ts +++ b/src/middleware/auth.ts @@ -1,7 +1,8 @@ -import { dbFunctions } from "~/core/database"; import { logger } from "~/core/utils/logger"; -import { config } from "~/typings/database"; +import { dbFunctions } from "~/core/database"; + import { set } from "~/typings/elysiajs"; +import { config } from "~/typings/database"; export async function hashApiKey(apiKey: string): Promise { logger.debug("Hashing API key"); @@ -16,7 +17,7 @@ export async function hashApiKey(apiKey: string): Promise { async function validateApiKeyHash( providedKey: string, - storedHash: string, + storedHash: string ): Promise { logger.debug("Validating API key hash"); try { @@ -30,7 +31,7 @@ async function validateApiKeyHash( } async function getApiKeyFromDb( - apiKey: string, + apiKey: string ): Promise<{ hash: string } | null> { const dbApiKey = (dbFunctions.getConfig() as config[])[0].api_key; logger.debug(`Querying database for API key: ${apiKey}`); @@ -44,7 +45,7 @@ export async function validateApiKey(request: Request, set: set) { if (process.env.NODE_ENV != "production") { logger.warn( - "API Key validation deactivated, since running in development mode", + "API Key validation deactivated, since running in development mode" ); return { apiKey }; } else if (!apiKey) { diff --git a/src/plugins/example.plugin.ts b/src/plugins/example.plugin.ts index d1a1ec6..e9a9775 100644 --- a/src/plugins/example.plugin.ts +++ b/src/plugins/example.plugin.ts @@ -1,6 +1,7 @@ +import { logger } from "~/core/utils/logger"; + import type { Plugin } from "~/typings/plugin"; import type { ContainerInfo } from "~/typings/docker"; -import { logger } from "~/core/utils/logger"; // See https://outline.itsnik.de/s/dockstat/doc/plugin-development-3UBj9gNMKF for more info @@ -9,67 +10,67 @@ const ExamplePlugin: Plugin = { async onContainerStart(containerInfo: ContainerInfo) { logger.info( - `Container ${containerInfo.name} started on ${containerInfo.hostId}`, + `Container ${containerInfo.name} started on ${containerInfo.hostId}` ); }, async onContainerStop(containerInfo: ContainerInfo) { logger.info( - `Container ${containerInfo.name} stopped on ${containerInfo.hostId}`, + `Container ${containerInfo.name} stopped on ${containerInfo.hostId}` ); }, async onContainerExit(containerInfo: ContainerInfo) { logger.info( - `Container ${containerInfo.name} exited on ${containerInfo.hostId}`, + `Container ${containerInfo.name} exited on ${containerInfo.hostId}` ); }, async onContainerCreate(containerInfo: ContainerInfo) { logger.info( - `Container ${containerInfo.name} created on ${containerInfo.hostId}`, + `Container ${containerInfo.name} created on ${containerInfo.hostId}` ); }, async onContainerDestroy(containerInfo: ContainerInfo) { logger.info( - `Container ${containerInfo.name} destroyed on ${containerInfo.hostId}`, + `Container ${containerInfo.name} destroyed on ${containerInfo.hostId}` ); }, async onContainerPause(containerInfo: ContainerInfo) { logger.info( - `Container ${containerInfo.name} pause on ${containerInfo.hostId}`, + `Container ${containerInfo.name} pause on ${containerInfo.hostId}` ); }, async onContainerUnpause(containerInfo: ContainerInfo) { logger.info( - `Container ${containerInfo.name} resumed on ${containerInfo.hostId}`, + `Container ${containerInfo.name} resumed on ${containerInfo.hostId}` ); }, async onContainerRestart(containerInfo: ContainerInfo) { logger.info( - `Container ${containerInfo.name} restarted on ${containerInfo.hostId}`, + `Container ${containerInfo.name} restarted on ${containerInfo.hostId}` ); }, async onContainerUpdate(containerInfo: ContainerInfo) { logger.info( - `Container ${containerInfo.name} updated on ${containerInfo.hostId}`, + `Container ${containerInfo.name} updated on ${containerInfo.hostId}` ); }, async onContainerRename(containerInfo: ContainerInfo) { logger.info( - `Container ${containerInfo.name} renamed on ${containerInfo.hostId}`, + `Container ${containerInfo.name} renamed on ${containerInfo.hostId}` ); }, async onContainerHealthStatus(containerInfo: ContainerInfo) { logger.info( - `Container ${containerInfo.name} changed status to ${containerInfo.status}`, + `Container ${containerInfo.name} changed status to ${containerInfo.status}` ); }, @@ -83,13 +84,13 @@ const ExamplePlugin: Plugin = { async handleContainerDie(containerInfo: ContainerInfo) { logger.info( - `Container ${containerInfo.name} died on ${containerInfo.hostId}`, + `Container ${containerInfo.name} died on ${containerInfo.hostId}` ); }, async onContainerKill(containerInfo: ContainerInfo) { logger.info( - `Container ${containerInfo.name} killed on ${containerInfo.hostId}`, + `Container ${containerInfo.name} killed on ${containerInfo.hostId}` ); }, } satisfies Plugin; diff --git a/src/plugins/telegram.plugin.ts b/src/plugins/telegram.plugin.ts index cf7c376..eaec24e 100644 --- a/src/plugins/telegram.plugin.ts +++ b/src/plugins/telegram.plugin.ts @@ -1,6 +1,7 @@ +import { logger } from "~/core/utils/logger"; + import type { Plugin } from "~/typings/plugin"; import type { ContainerInfo } from "~/typings/docker"; -import { logger } from "~/core/utils/logger"; const TELEGRAM_BOT_TOKEN = "CHANGE_ME"; // Replace with your bot token const TELEGRAM_CHAT_ID = "CHANGE_ME"; // Replace with your chat ID @@ -19,7 +20,7 @@ const TelegramNotificationPlugin: Plugin = { chat_id: TELEGRAM_CHAT_ID, text: message, }), - }, + } ); if (!response.ok) { logger.error(`HTTP error ${response.status}`); diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index 9861d3f..0b2c4fb 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -1,8 +1,9 @@ import { Elysia, t } from "elysia"; -import { dbFunctions } from "~/core/database"; + import { logger } from "~/core/utils/logger"; +import { dbFunctions } from "~/core/database"; +import { pluginManager } from "~/core/plugins/plugin-manager"; import { responseHandler } from "~/core/utils/response-handler"; -import { config } from "~/typings/database"; import { version, authorEmail, @@ -14,8 +15,10 @@ import { devDependencies, license, } from "~/core/utils/package-json"; + import { hashApiKey } from "~/middleware/auth"; -import { pluginManager } from "~/core/plugins/plugin-manager"; + +import { config } from "~/typings/database"; export const apiConfigRoutes = new Elysia({ prefix: "/config" }) .get( @@ -32,7 +35,7 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) return responseHandler.error( set, error as string, - "Error getting the DockStatAPI config", + "Error getting the DockStatAPI config" ); } }, @@ -42,7 +45,7 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) description: "Returns current API configuration including data retention policies and security settings", }, - }, + } ) .get( "/plugins", @@ -53,7 +56,7 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) return responseHandler.error( set, error as string, - "Error getting all registered plugins", + "Error getting all registered plugins" ); } }, @@ -63,7 +66,7 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) description: "Lists all active plugins with their registration details and status", }, - }, + } ) .post( "/update", @@ -74,14 +77,14 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) dbFunctions.updateConfig( fetching_interval, keep_data_for, - await hashApiKey(api_key), + await hashApiKey(api_key) ); return responseHandler.ok(set, "Updated DockStatAPI config"); } catch (error) { return responseHandler.error( set, "Error updating the DockStatAPI config", - error as string, + error as string ); } }, @@ -96,7 +99,7 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) description: "Modifies core API settings including data collection intervals, retention periods, and security credentials", }, - }, + } ) .get( "/package", @@ -118,7 +121,7 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) return responseHandler.error( set, error as string, - "Error while reading package.json", + "Error while reading package.json" ); } }, @@ -128,5 +131,5 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) description: "Displays package metadata including dependencies, contributors, and licensing information", }, - }, + } ); diff --git a/src/routes/docker-manager.ts b/src/routes/docker-manager.ts index 635b78b..3574751 100644 --- a/src/routes/docker-manager.ts +++ b/src/routes/docker-manager.ts @@ -1,7 +1,9 @@ import { Elysia, t } from "elysia"; -import { dbFunctions } from "~/core/database"; + import { logger } from "~/core/utils/logger"; +import { dbFunctions } from "~/core/database"; import { responseHandler } from "~/core/utils/response-handler"; + import { DockerHost } from "~/typings/docker"; export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) @@ -16,7 +18,7 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) return responseHandler.error( set, "Error adding docker Host", - error as string, + error as string ); } }, @@ -31,7 +33,7 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) hostAddress: t.String(), secure: t.Boolean(), }), - }, + } ) .post( @@ -45,7 +47,7 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) return responseHandler.error( set, error as string, - "Failed to update host", + "Failed to update host" ); } }, @@ -61,7 +63,7 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) hostAddress: t.String(), secure: t.Boolean(), }), - }, + } ) .get( @@ -76,7 +78,7 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) return responseHandler.error( set, error as string, - "Failed to retrieve hosts", + "Failed to retrieve hosts" ); } }, @@ -86,7 +88,7 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) description: "Lists all configured Docker hosts with their connection settings", }, - }, + } ) .delete( @@ -100,7 +102,7 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) return responseHandler.error( set, error as string, - "Failed to delete host", + "Failed to delete host" ); } }, @@ -113,5 +115,5 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) params: t.Object({ id: t.Number(), }), - }, + } ); diff --git a/src/routes/docker-stats.ts b/src/routes/docker-stats.ts index 9cfe465..3c31c5c 100644 --- a/src/routes/docker-stats.ts +++ b/src/routes/docker-stats.ts @@ -1,16 +1,18 @@ import Docker from "dockerode"; import { Elysia } from "elysia"; + +import { logger } from "~/core/utils/logger"; import { dbFunctions } from "~/core/database"; import { getDockerClient } from "~/core/docker/client"; +import { findObjectByKey } from "~/core/utils/helpers"; +import { responseHandler } from "~/core/utils/response-handler"; import { calculateCpuPercent, calculateMemoryUsage, } from "~/core/utils/calculations"; -import { logger } from "~/core/utils/logger"; -import { responseHandler } from "~/core/utils/response-handler"; -import { findObjectByKey } from "~/core/utils/helpers"; -import type { ContainerInfo, DockerHost, HostStats } from "~/typings/docker"; + import type { DockerInfo } from "~/typings/dockerode"; +import type { ContainerInfo, DockerHost, HostStats } from "~/typings/docker"; export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) .get( diff --git a/src/routes/docker-websocket.ts b/src/routes/docker-websocket.ts index 8b26542..3165eff 100644 --- a/src/routes/docker-websocket.ts +++ b/src/routes/docker-websocket.ts @@ -1,15 +1,16 @@ +import split2 from "split2"; import { Elysia } from "elysia"; +import type { Readable } from "stream"; import type { ElysiaWS } from "elysia/dist/ws"; + +import { logger } from "~/core/utils/logger"; import { dbFunctions } from "~/core/database"; import { getDockerClient } from "~/core/docker/client"; +import { responseHandler } from "~/core/utils/response-handler"; import { calculateCpuPercent, calculateMemoryUsage, } from "~/core/utils/calculations"; -import { logger } from "~/core/utils/logger"; -import { responseHandler } from "~/core/utils/response-handler"; -import split2 from "split2"; -import type { Readable } from "stream"; const activeDockerConnections = new Set>(); const connectionStreams = new Map< diff --git a/src/routes/live-logs.ts b/src/routes/live-logs.ts index 1232ce5..17c4c69 100644 --- a/src/routes/live-logs.ts +++ b/src/routes/live-logs.ts @@ -1,7 +1,9 @@ import { Elysia } from "elysia"; import type { ElysiaWS } from "elysia/dist/ws"; + import { logger } from "~/core/utils/logger"; -import type { logStreamData } from "~/typings/websocket"; + +import { log_message } from "~/typings/database"; const activeConnections = new Set>(); @@ -17,7 +19,7 @@ export const liveLogs = new Elysia({ prefix: "/logs" }).ws("/ws", { }, }); -export function logToClients(data: logStreamData) { +export function logToClients(data: log_message) { activeConnections.forEach((ws) => { try { ws.send(JSON.stringify(data)); diff --git a/src/routes/live-stacks.ts b/src/routes/live-stacks.ts new file mode 100644 index 0000000..4f3d395 --- /dev/null +++ b/src/routes/live-stacks.ts @@ -0,0 +1,30 @@ +import { Elysia } from "elysia"; +import type { ElysiaWS } from "elysia/dist/ws"; + +import { logger } from "~/core/utils/logger"; +import { stackSocketMessage } from "~/typings/websocket"; + +const activeConnections = new Set>(); + +export const liveStacks = new Elysia().ws("/stacks", { + open(ws) { + activeConnections.add(ws); + ws.send({ message: "Connection established" }); + logger.info(`New Stacks WebSocket established (${ws.id})`); + }, + close(ws) { + logger.info(`Stacks WebSocket closed (${ws.id})`); + activeConnections.delete(ws); + }, +}); + +export function postToClient(data: stackSocketMessage) { + activeConnections.forEach((ws) => { + try { + ws.send(JSON.stringify(data)); + } catch (error) { + activeConnections.delete(ws); + logger.error("Failed to send to WebSocket:", error); + } + }); +} diff --git a/src/routes/logs.ts b/src/routes/logs.ts index dc7fc37..ce33235 100644 --- a/src/routes/logs.ts +++ b/src/routes/logs.ts @@ -1,6 +1,7 @@ import { Elysia } from "elysia"; -import { dbFunctions } from "~/core/database"; + import { logger } from "~/core/utils/logger"; +import { dbFunctions } from "~/core/database"; export const backendLogs = new Elysia({ prefix: "/logs" }) .get( @@ -23,7 +24,7 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) description: "Retrieves complete application log history from persistent storage", }, - }, + } ) .get( @@ -46,7 +47,7 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) description: "Filters logs by severity level (debug, info, warn, error, fatal)", }, - }, + } ) .delete( @@ -68,7 +69,7 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) tags: ["Management"], description: "Purges all historical log records from the database", }, - }, + } ) .delete( @@ -90,5 +91,5 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) tags: ["Management"], description: "Clears log entries matching specified severity level", }, - }, + } ); diff --git a/src/routes/stacks.ts b/src/routes/stacks.ts index eea4160..528a5c1 100644 --- a/src/routes/stacks.ts +++ b/src/routes/stacks.ts @@ -1,4 +1,7 @@ import { Elysia, t } from "elysia"; + +import { logger } from "~/core/utils/logger"; +import { dbFunctions } from "~/core/database"; import { responseHandler } from "~/core/utils/response-handler"; import { deployStack, @@ -10,8 +13,6 @@ import { getAllStacksStatus, removeStack, } from "~/core/stacks/controller"; -import { dbFunctions } from "~/core/database"; -import { logger } from "~/core/utils/logger"; export const stackRoutes = new Elysia({ prefix: "/stacks" }) .post( @@ -49,18 +50,18 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) body.automatic_reboot_on_error, isCustom, image_updates, - body.stack_prefix, + body.stack_prefix ); logger.info(`Deployed Stack (${body.name})`); return responseHandler.ok( set, - `Stack ${body.name} deployed successfully`, + `Stack ${body.name} deployed successfully` ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error deploying stack", + "Error deploying stack" ); } }, @@ -80,7 +81,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) source: t.String(), stack_prefix: t.Optional(t.String()), }), - }, + } ) .post( "/start", @@ -93,13 +94,13 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) logger.info(`Started Stack (${body.stackId})`); return responseHandler.ok( set, - `Stack ${body.stackId} started successfully`, + `Stack ${body.stackId} started successfully` ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error starting stack", + "Error starting stack" ); } }, @@ -112,7 +113,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) body: t.Object({ stackId: t.Number(), }), - }, + } ) .post( "/stop", @@ -125,13 +126,13 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) logger.info(`Stopped Stack (${body.stackId})`); return responseHandler.ok( set, - `Stack ${body.stackId} stopped successfully`, + `Stack ${body.stackId} stopped successfully` ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error stopping stack", + "Error stopping stack" ); } }, @@ -144,7 +145,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) body: t.Object({ stackId: t.Number(), }), - }, + } ) .post( "/restart", @@ -157,13 +158,13 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) logger.info(`Restarted Stack (${body.stackId})`); return responseHandler.ok( set, - `Stack ${body.stackId} restarted successfully`, + `Stack ${body.stackId} restarted successfully` ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error restarting stack", + "Error restarting stack" ); } }, @@ -176,7 +177,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) body: t.Object({ stackId: t.Number(), }), - }, + } ) .post( "/pull-images", @@ -189,13 +190,13 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) logger.info(`Pulled Stack images (${body.stackId})`); return responseHandler.ok( set, - `Images for stack ${body.stackId} pulled successfully`, + `Images for stack ${body.stackId} pulled successfully` ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error pulling images", + "Error pulling images" ); } }, @@ -208,7 +209,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) body: t.Object({ stackId: t.Number(), }), - }, + } ) .get( "/status", @@ -220,7 +221,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) status = await getStackStatus(query.stackId); res = responseHandler.ok( set, - `Stack ${query.stackId} status retrieved successfully`, + `Stack ${query.stackId} status retrieved successfully` ); logger.info("Fetched Stack status"); } else { @@ -233,7 +234,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) return responseHandler.error( set, error.message || error, - "Error getting stack status", + "Error getting stack status" ); } }, @@ -246,7 +247,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) query: t.Object({ stackId: t.Number(), }), - }, + } ) .get( "/", @@ -259,7 +260,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) return responseHandler.error( set, error.message || error, - "Error getting stacks", + "Error getting stacks" ); } }, @@ -269,7 +270,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) description: "Lists all registered stacks with their complete configuration details", }, - }, + } ) .delete( @@ -284,7 +285,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) return responseHandler.error( set, error.message || error, - "Error deleting stack", + "Error deleting stack" ); } }, @@ -297,5 +298,5 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) body: t.Object({ stackId: t.Number(), }), - }, + } ); diff --git a/src/routes/utils.ts b/src/routes/utils.ts index b61e0e7..17cba24 100644 --- a/src/routes/utils.ts +++ b/src/routes/utils.ts @@ -1,4 +1,6 @@ import { Elysia, t } from "elysia"; + +import { responseHandler } from "~/core/utils/response-handler"; import { version, authorEmail, @@ -10,7 +12,6 @@ import { devDependencies, license, } from "~/core/utils/package-json"; -import { responseHandler } from "~/core/utils/response-handler"; export const utilRoutes = new Elysia({ prefix: "/utils" }).get( "/info", @@ -32,7 +33,7 @@ export const utilRoutes = new Elysia({ prefix: "/utils" }).get( return responseHandler.error( set, error.message || error, - "Error getting DockStatAPI information", + "Error getting DockStatAPI information" ); } }, @@ -42,5 +43,5 @@ export const utilRoutes = new Elysia({ prefix: "/utils" }).get( description: "Retrieves DockStatAPI metadata including version, author information, dependencies, and licensing details", }, - }, + } ); diff --git a/src/tests/cleanup.ts b/src/tests/cleanup.ts index 20b965b..31756dd 100644 --- a/src/tests/cleanup.ts +++ b/src/tests/cleanup.ts @@ -1,7 +1,8 @@ import { dbFunctions } from "~/core/database"; -import type { DockerHost } from "~/typings/docker"; import { findObjectByKey } from "~/core/utils/helpers"; +import type { DockerHost } from "~/typings/docker"; + console.log(""); console.log("Deleting `test` Docker host"); diff --git a/src/tests/delete.spec.ts b/src/tests/delete.spec.ts index 37e36fc..097ede0 100644 --- a/src/tests/delete.spec.ts +++ b/src/tests/delete.spec.ts @@ -1,4 +1,5 @@ import { describe, it } from "bun:test"; + import { runTestCode } from "./helper"; describe("DockStatAPI (DELETE)", () => { diff --git a/src/tests/gets.spec.ts b/src/tests/gets.spec.ts index 2723508..7b10315 100644 --- a/src/tests/gets.spec.ts +++ b/src/tests/gets.spec.ts @@ -1,5 +1,5 @@ import { describe, it } from "bun:test"; -import { runTestResponse, runTestCode } from "./helper"; + import { version, authorEmail, @@ -12,6 +12,8 @@ import { license, } from "~/core/utils/package-json"; +import { runTestResponse, runTestCode } from "./helper"; + describe("DockStatAPI (GET)", () => { it("Check Server connection", async () => { await runTestResponse("/health", '{"status":"healthy"}', "GET"); diff --git a/src/tests/helper.ts b/src/tests/helper.ts index bd03055..59ec039 100644 --- a/src/tests/helper.ts +++ b/src/tests/helper.ts @@ -1,7 +1,9 @@ import { expect } from "bun:test"; -import { DockStatAPI } from ".."; + import { logger } from "~/core/utils/logger"; +import { DockStatAPI } from ".."; + export const API_KEY = "TestKey"; const server = "http://localhost:3001"; diff --git a/src/tests/post.spec.ts b/src/tests/post.spec.ts index 9ce64e9..9747e67 100644 --- a/src/tests/post.spec.ts +++ b/src/tests/post.spec.ts @@ -1,5 +1,7 @@ import { describe, it } from "bun:test"; + import { runTestResponse, runTestCode } from "./helper"; + import { DockerHost } from "~/typings/docker"; describe("DockStatAPI (POST)", () => { @@ -36,7 +38,7 @@ describe("DockStatAPI (POST)", () => { await runTestResponse( "/docker-config/hosts", JSON.stringify(responseBody), - "GET", + "GET" ); }); diff --git a/src/typings/database.ts b/src/typings/database.ts index 96319e9..67d8121 100644 --- a/src/typings/database.ts +++ b/src/typings/database.ts @@ -16,4 +16,12 @@ interface stacks_config { image_updates: boolean; } -export type { config, stacks_config }; +interface log_message { + level: string; + timestamp: string; + message: string; + file: string; + line: number; +} + +export type { config, stacks_config, log_message }; diff --git a/src/typings/plugin.ts b/src/typings/plugin.ts index ee16559..6ca68bf 100644 --- a/src/typings/plugin.ts +++ b/src/typings/plugin.ts @@ -1,5 +1,4 @@ import { ContainerInfo } from "~/typings/docker"; -import { HostStats } from "~/typings/docker"; interface Plugin { name: string; diff --git a/src/typings/websocket.ts b/src/typings/websocket.ts index 1cb3821..5635e3c 100644 --- a/src/typings/websocket.ts +++ b/src/typings/websocket.ts @@ -1,17 +1,15 @@ -//import type { Readable, Transform } from "stream"; -//import type internal from "stream"; - -//interface streams { -// statsStream: Readable; -// splitStream: internal.Transform; -//} +interface stackSocketMessage { + message?: string; + type?: "stack-progress" | "stack-error" | "stack-status" | "stack-removed"; + data?: stackSocketData; +} -interface logStreamData { - timestamp: string; - level: string; +interface stackSocketData { + stack_id: number; message: string; - file: string; - line: number; + action?: string; + status?: string; + timestamp?: string; } -export { logStreamData }; +export { stackSocketMessage }; diff --git a/tsconfig.json b/tsconfig.json index ab566ad..9c2d511 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,7 +23,7 @@ // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ - + "outDir": "build/", /* Modules */ "module": "ES2022" /* Specify what module code is generated. */, // "rootDir": "./", /* Specify the root folder within your source files. */ From d62c682949e9930f16c03743c98ef89dc1d39bb8 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Tue, 15 Apr 2025 17:45:35 +0000 Subject: [PATCH 238/324] Update dependency graphs --- dependency-graph.mmd | 357 +++++------ dependency-graph.svg | 1349 ++++++++++++++++++++++-------------------- 2 files changed, 881 insertions(+), 825 deletions(-) diff --git a/dependency-graph.mmd b/dependency-graph.mmd index 844ace0..1ba6254 100644 --- a/dependency-graph.mmd +++ b/dependency-graph.mmd @@ -8,203 +8,210 @@ flowchart LR subgraph 0["src"] 1["index.ts"] -subgraph 2["core"] -subgraph 3["docker"] -4["monitor.ts"] -V["client.ts"] -1A["scheduler.ts"] -1B["store-host-stats.ts"] -1D["store-container-stats.ts"] +subgraph 2["routes"] +3["live-stacks.ts"] +P["live-logs.ts"] +1D["api-config.ts"] +1F["docker-manager.ts"] +1G["docker-stats.ts"] +1H["docker-websocket.ts"] +1J["logs.ts"] +1K["stacks.ts"] +1N["utils.ts"] end -subgraph 6["plugins"] -7["plugin-manager.ts"] -1F["loader.ts"] +subgraph 4["core"] +subgraph 5["utils"] +6["logger.ts"] +M["helpers.ts"] +10["calculations.ts"] +15["change-me-checker.ts"] +17["package-json.ts"] +19["swagger-readme.ts"] +1E["response-handler.ts"] end -subgraph 9["utils"] -A["logger.ts"] -W["swagger-readme.ts"] -15["helpers.ts"] -16["response-handler.ts"] -18["package-json.ts"] -1E["calculations.ts"] -1G["change-me-checker.ts"] +subgraph 8["database"] +9["index.ts"] +A["config.ts"] +B["database.ts"] +D["helper.ts"] +E["containerStats.ts"] +F["dockerHosts.ts"] +I["hostStats.ts"] +J["logs.ts"] +L["stacks.ts"] end -subgraph C["database"] -D["index.ts"] -E["config.ts"] -F["database.ts"] -H["helper.ts"] -I["containerStats.ts"] -J["dockerHosts.ts"] -M["hostStats.ts"] -N["logs.ts"] -P["stacks.ts"] +subgraph Q["docker"] +R["monitor.ts"] +X["client.ts"] +Y["scheduler.ts"] +Z["store-container-stats.ts"] +11["store-host-stats.ts"] end -subgraph 11["stacks"] -12["controller.ts"] +subgraph T["plugins"] +U["plugin-manager.ts"] +13["loader.ts"] end +subgraph 1L["stacks"] +1M["controller.ts"] end -subgraph K["typings"] -L["docker.ts"] -O["websocket.ts"] -Q["database.ts"] -R["docker-compose.ts"] -U["plugin.ts"] -Z["elysiajs.ts"] -1C["dockerode.ts"] end -subgraph S["routes"] -T["live-logs.ts"] -10["stacks.ts"] -17["utils.ts"] -1H["api-config.ts"] -1I["docker-manager.ts"] -1J["docker-stats.ts"] -1K["docker-websocket.ts"] -1M["logs.ts"] +subgraph G["typings"] +H["docker.ts"] +K["websocket.ts"] +N["database.ts"] +O["docker-compose.ts"] +W["plugin.ts"] +12["dockerode.ts"] +1C["elysiajs.ts"] end -subgraph X["middleware"] -Y["auth.ts"] +subgraph 1A["middleware"] +1B["auth.ts"] end end -5["bun"] -8["events"] -B["path"] -G["bun:sqlite"] -subgraph 13["fs"] -14["promises"] +7["path"] +C["bun:sqlite"] +S["bun"] +V["events"] +subgraph 14["fs"] +16["promises"] end -19["package.json"] -1L["stream"] -1-->4 -1-->W +18["package.json"] +1I["stream"] +1-->3 +1-->9 +1-->R 1-->Y -1-->T -1-->10 +1-->13 +1-->6 1-->17 -1-->Q -1-->D -1-->1A +1-->19 +1-->1B +1-->1D 1-->1F -1-->A +1-->1G 1-->1H -1-->1I +1-->P 1-->1J 1-->1K -1-->1M -4-->7 -4-->D -4-->V -4-->A -4-->L -4-->L -4-->5 -7-->A -7-->L -7-->U -7-->8 -A-->D -A-->T -A-->O +1-->1N +1-->N +3-->6 +3-->K +6-->9 +6-->P +6-->N +6-->7 +9-->A +9-->E +9-->B +9-->F +9-->I +9-->J +9-->L A-->B -D-->E -D-->I -D-->F -D-->J -D-->M -D-->N -D-->P -E-->F -E-->H -F-->G -H-->A -I-->F +A-->D +B-->C +D-->6 +E-->B +E-->D +F-->B +F-->D +F-->H +I-->B +I-->D I-->H -J-->F -J-->H -J-->L -M-->F -M-->H -M-->L -N-->F -N-->H -N-->O -P-->F -P-->H -P-->Q -P-->R -T-->A -T-->O -U-->L -V-->A -V-->L -Y-->D -Y-->A -Y-->Q +J-->B +J-->D +J-->K +L-->M +L-->B +L-->D +L-->N +L-->O +M-->6 +P-->6 +P-->N +R-->U +R-->9 +R-->X +R-->6 +R-->H +R-->H +R-->S +U-->6 +U-->H +U-->W +U-->V +W-->H +X-->6 +X-->H +Y-->9 Y-->Z -10-->D -10-->12 -10-->A -10-->16 -12-->15 -12-->D -12-->A -12-->Q -12-->R -12-->14 -15-->A -16-->A -16-->Z +Y-->11 +Y-->6 +Y-->N +Z-->6 +Z-->9 +Z-->X +Z-->10 +11-->9 +11-->X +11-->M +11-->6 +11-->H +11-->12 +13-->15 +13-->6 +13-->U +13-->14 +13-->7 +15-->6 +15-->16 17-->18 -17-->16 -18-->19 -1A-->D -1A-->1B -1A-->1D -1A-->A -1A-->Q -1B-->D -1B-->V -1B-->15 -1B-->A -1B-->L +1B-->9 +1B-->6 +1B-->N 1B-->1C -1D-->A -1D-->D -1D-->V +1D-->9 +1D-->U +1D-->6 +1D-->17 1D-->1E -1F-->1G -1F-->A -1F-->7 -1F-->13 -1F-->B -1G-->A -1G-->14 -1H-->D -1H-->7 -1H-->A -1H-->18 -1H-->16 -1H-->Y -1H-->Q -1I-->D -1I-->A -1I-->16 -1I-->L -1J-->D -1J-->V -1J-->1E -1J-->15 -1J-->A -1J-->16 -1J-->L -1J-->1C -1K-->D -1K-->V +1D-->1B +1D-->N +1E-->6 +1E-->1C +1F-->9 +1F-->6 +1F-->1E +1F-->H +1G-->9 +1G-->X +1G-->10 +1G-->M +1G-->6 +1G-->1E +1G-->H +1G-->12 +1H-->9 +1H-->X +1H-->10 +1H-->6 +1H-->1E +1H-->1I +1J-->9 +1J-->6 +1K-->9 +1K-->1M +1K-->6 1K-->1E -1K-->A -1K-->16 -1K-->1L -1M-->D -1M-->A +1M-->M +1M-->9 +1M-->6 +1M-->3 +1M-->N +1M-->O +1M-->16 +1N-->17 +1N-->1E diff --git a/dependency-graph.svg b/dependency-graph.svg index 0549c66..34974a2 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -4,72 +4,72 @@ - - + + dependency-cruiser output - + cluster_fs - -fs + +fs cluster_src - -src + +src cluster_src/core - -core + +core cluster_src/core/database - -database + +database cluster_src/core/docker - -docker + +docker cluster_src/core/plugins - -plugins + +plugins cluster_src/core/stacks - -stacks + +stacks cluster_src/core/utils - -utils + +utils cluster_src/middleware - -middleware + +middleware cluster_src/routes - -routes + +routes cluster_src/typings - -typings + +typings bun - -bun + +bun @@ -77,8 +77,8 @@ bun:sqlite - -bun:sqlite + +bun:sqlite @@ -86,8 +86,8 @@ events - -events + +events @@ -95,8 +95,8 @@ fs - -fs + +fs @@ -104,8 +104,8 @@ fs/promises - -promises + +promises @@ -113,8 +113,8 @@ package.json - -package.json + +package.json @@ -122,8 +122,8 @@ path - -path + +path @@ -131,8 +131,8 @@ src/core/database/config.ts - -config.ts + +config.ts @@ -140,1182 +140,1231 @@ src/core/database/database.ts - -database.ts + +database.ts src/core/database/config.ts->src/core/database/database.ts - - + + src/core/database/helper.ts - -helper.ts + +helper.ts src/core/database/config.ts->src/core/database/helper.ts - - - - + + + + src/core/database/database.ts->bun:sqlite - - + + src/core/utils/logger.ts - -logger.ts + +logger.ts src/core/database/helper.ts->src/core/utils/logger.ts - - - - + + + + src/core/database/containerStats.ts - -containerStats.ts + +containerStats.ts src/core/database/containerStats.ts->src/core/database/database.ts - - + + src/core/database/containerStats.ts->src/core/database/helper.ts - - - - + + + + src/core/database/dockerHosts.ts - -dockerHosts.ts + +dockerHosts.ts src/core/database/dockerHosts.ts->src/core/database/database.ts - - + + src/core/database/dockerHosts.ts->src/core/database/helper.ts - - - - + + + + src/typings/docker.ts - -docker.ts + +docker.ts src/core/database/dockerHosts.ts->src/typings/docker.ts - - + + - + src/core/utils/logger.ts->path - - + + src/core/database/index.ts - -index.ts + +index.ts - + src/core/utils/logger.ts->src/core/database/index.ts - - - - + + + + - - -src/typings/websocket.ts - - -websocket.ts + + +src/typings/database.ts + + +database.ts - - -src/core/utils/logger.ts->src/typings/websocket.ts - - + + +src/core/utils/logger.ts->src/typings/database.ts + + - + src/routes/live-logs.ts - - -live-logs.ts + + +live-logs.ts - + src/core/utils/logger.ts->src/routes/live-logs.ts - - - - + + + + src/core/database/hostStats.ts - -hostStats.ts + +hostStats.ts src/core/database/hostStats.ts->src/core/database/database.ts - - + + src/core/database/hostStats.ts->src/core/database/helper.ts - - - - + + + + src/core/database/hostStats.ts->src/typings/docker.ts - - + + src/core/database/index.ts->src/core/database/config.ts - - - - + + + + src/core/database/index.ts->src/core/database/database.ts - - + + src/core/database/index.ts->src/core/database/containerStats.ts - - - - + + + + src/core/database/index.ts->src/core/database/dockerHosts.ts - - - - + + + + src/core/database/index.ts->src/core/database/hostStats.ts - - - - + + + + src/core/database/logs.ts - -logs.ts + +logs.ts src/core/database/index.ts->src/core/database/logs.ts - - - - + + + + src/core/database/stacks.ts - -stacks.ts + +stacks.ts src/core/database/index.ts->src/core/database/stacks.ts - - - - + + + + src/core/database/logs.ts->src/core/database/database.ts - - + + src/core/database/logs.ts->src/core/database/helper.ts - - - - + + + + + + + +src/typings/websocket.ts + + +websocket.ts + + src/core/database/logs.ts->src/typings/websocket.ts - - + + - + src/core/database/stacks.ts->src/core/database/database.ts - - + + - + src/core/database/stacks.ts->src/core/database/helper.ts - - - - + + + + - + -src/typings/database.ts - - -database.ts +src/core/utils/helpers.ts + + +helpers.ts + + +src/core/database/stacks.ts->src/core/utils/helpers.ts + + + + + - + src/core/database/stacks.ts->src/typings/database.ts - - + + - + src/typings/docker-compose.ts - - -docker-compose.ts + + +docker-compose.ts - + src/core/database/stacks.ts->src/typings/docker-compose.ts - - + + + + + +src/core/utils/helpers.ts->src/core/utils/logger.ts + + + + - + src/core/docker/client.ts - - -client.ts + + +client.ts - + src/core/docker/client.ts->src/typings/docker.ts - - + + - + src/core/docker/client.ts->src/core/utils/logger.ts - - + + - + src/core/docker/monitor.ts - - -monitor.ts + + +monitor.ts - + src/core/docker/monitor.ts->bun - - + + - + src/core/docker/monitor.ts->src/typings/docker.ts - - + + - + src/core/docker/monitor.ts->src/core/utils/logger.ts - - + + - + src/core/docker/monitor.ts->src/core/database/index.ts - - + + - + src/core/docker/monitor.ts->src/core/docker/client.ts - - + + - + src/core/plugins/plugin-manager.ts - - -plugin-manager.ts + + +plugin-manager.ts - + src/core/docker/monitor.ts->src/core/plugins/plugin-manager.ts - - + + - + src/core/plugins/plugin-manager.ts->events - - + + - + src/core/plugins/plugin-manager.ts->src/typings/docker.ts - - + + - + src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts - - + + src/typings/plugin.ts - -plugin.ts + +plugin.ts - + src/core/plugins/plugin-manager.ts->src/typings/plugin.ts - - + + - + src/core/docker/scheduler.ts - - -scheduler.ts + + +scheduler.ts - + src/core/docker/scheduler.ts->src/core/utils/logger.ts - - + + - + src/core/docker/scheduler.ts->src/core/database/index.ts - - + + - + src/core/docker/scheduler.ts->src/typings/database.ts - - + + + + + +src/core/docker/store-container-stats.ts + + +store-container-stats.ts + + + + + +src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts + + - + src/core/docker/store-host-stats.ts - - -store-host-stats.ts + + +store-host-stats.ts - + src/core/docker/scheduler.ts->src/core/docker/store-host-stats.ts - - + + - - -src/core/docker/store-container-stats.ts - - -store-container-stats.ts + + +src/core/docker/store-container-stats.ts->src/core/utils/logger.ts + + + + + +src/core/docker/store-container-stats.ts->src/core/database/index.ts + + + + + +src/core/docker/store-container-stats.ts->src/core/docker/client.ts + + + + + +src/core/utils/calculations.ts + + +calculations.ts - - -src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts - - + + +src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts + + - + src/core/docker/store-host-stats.ts->src/typings/docker.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/utils/logger.ts - - + + - -src/core/docker/store-host-stats.ts->src/core/database/index.ts - - - - -src/core/docker/store-host-stats.ts->src/core/docker/client.ts - - - - - -src/core/utils/helpers.ts - - -helpers.ts - - +src/core/docker/store-host-stats.ts->src/core/database/index.ts + + - + src/core/docker/store-host-stats.ts->src/core/utils/helpers.ts - - + + + + + +src/core/docker/store-host-stats.ts->src/core/docker/client.ts + + src/typings/dockerode.ts - -dockerode.ts + +dockerode.ts - + src/core/docker/store-host-stats.ts->src/typings/dockerode.ts - - - - - -src/core/docker/store-container-stats.ts->src/core/utils/logger.ts - - - - - -src/core/docker/store-container-stats.ts->src/core/database/index.ts - - - - - -src/core/docker/store-container-stats.ts->src/core/docker/client.ts - - - - - -src/core/utils/calculations.ts - - -calculations.ts - - - - - -src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts - - - - - -src/core/utils/helpers.ts->src/core/utils/logger.ts - - + + src/core/plugins/loader.ts - -loader.ts + +loader.ts - + src/core/plugins/loader.ts->fs - - + + - + src/core/plugins/loader.ts->path - - + + - + src/core/plugins/loader.ts->src/core/utils/logger.ts - - + + - + src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts - - + + src/core/utils/change-me-checker.ts - -change-me-checker.ts + +change-me-checker.ts - + src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts - - + + - + src/core/utils/change-me-checker.ts->fs/promises - - + + - + src/core/utils/change-me-checker.ts->src/core/utils/logger.ts - - + + - + src/typings/plugin.ts->src/typings/docker.ts - - + + src/core/stacks/controller.ts - -controller.ts + +controller.ts - + src/core/stacks/controller.ts->fs/promises - - + + - + src/core/stacks/controller.ts->src/core/utils/logger.ts - - + + - + src/core/stacks/controller.ts->src/core/database/index.ts - - + + + + + +src/core/stacks/controller.ts->src/core/utils/helpers.ts + + - + src/core/stacks/controller.ts->src/typings/database.ts - - + + - + src/core/stacks/controller.ts->src/typings/docker-compose.ts - - + + - - -src/core/stacks/controller.ts->src/core/utils/helpers.ts - - + + +src/routes/live-stacks.ts + + +live-stacks.ts + + + + + +src/core/stacks/controller.ts->src/routes/live-stacks.ts + + + + + +src/routes/live-stacks.ts->src/core/utils/logger.ts + + + + + +src/routes/live-stacks.ts->src/typings/websocket.ts + + - + src/routes/live-logs.ts->src/core/utils/logger.ts - - - - + + + + - - -src/routes/live-logs.ts->src/typings/websocket.ts - - + + +src/routes/live-logs.ts->src/typings/database.ts + + - + src/core/utils/package-json.ts - - -package-json.ts + + +package-json.ts - + src/core/utils/package-json.ts->package.json - - + + - + src/core/utils/response-handler.ts - - -response-handler.ts + + +response-handler.ts - + src/core/utils/response-handler.ts->src/core/utils/logger.ts - - + + - + src/typings/elysiajs.ts - - -elysiajs.ts + + +elysiajs.ts - + src/core/utils/response-handler.ts->src/typings/elysiajs.ts - - + + - + src/core/utils/swagger-readme.ts - - -swagger-readme.ts + + +swagger-readme.ts - + src/index.ts - - -index.ts + + +index.ts - + src/index.ts->src/core/utils/logger.ts - - + + - + src/index.ts->src/core/database/index.ts - - + + - + src/index.ts->src/typings/database.ts - - + + - + src/index.ts->src/core/docker/monitor.ts - - + + - + src/index.ts->src/core/docker/scheduler.ts - - + + - + src/index.ts->src/core/plugins/loader.ts - - + + + + + +src/index.ts->src/routes/live-stacks.ts + + - + src/index.ts->src/routes/live-logs.ts - - + + + + + +src/index.ts->src/core/utils/package-json.ts + + - + src/index.ts->src/core/utils/swagger-readme.ts - - + + - + src/middleware/auth.ts - - -auth.ts + + +auth.ts - + src/index.ts->src/middleware/auth.ts - - - - - -src/routes/stacks.ts - - -stacks.ts - - - - - -src/index.ts->src/routes/stacks.ts - - - - - -src/routes/utils.ts - - -utils.ts - - - - - -src/index.ts->src/routes/utils.ts - - + + - + src/routes/api-config.ts - - -api-config.ts + + +api-config.ts src/index.ts->src/routes/api-config.ts - - + + - + src/routes/docker-manager.ts - - -docker-manager.ts + + +docker-manager.ts src/index.ts->src/routes/docker-manager.ts - - + + - + src/routes/docker-stats.ts - - -docker-stats.ts + + +docker-stats.ts src/index.ts->src/routes/docker-stats.ts - - + + - + src/routes/docker-websocket.ts - - -docker-websocket.ts + + +docker-websocket.ts src/index.ts->src/routes/docker-websocket.ts - - + + - + src/routes/logs.ts - - -logs.ts + + +logs.ts - + src/index.ts->src/routes/logs.ts - - + + - + + +src/routes/stacks.ts + + +stacks.ts + + + + +src/index.ts->src/routes/stacks.ts + + + + + +src/routes/utils.ts + + +utils.ts + + + + + +src/index.ts->src/routes/utils.ts + + + + + src/middleware/auth.ts->src/core/utils/logger.ts - - + + - + src/middleware/auth.ts->src/core/database/index.ts - - + + - + src/middleware/auth.ts->src/typings/database.ts - - + + - + src/middleware/auth.ts->src/typings/elysiajs.ts - - - - - -src/routes/stacks.ts->src/core/utils/logger.ts - - - - - -src/routes/stacks.ts->src/core/database/index.ts - - - - - -src/routes/stacks.ts->src/core/stacks/controller.ts - - - - - -src/routes/stacks.ts->src/core/utils/response-handler.ts - - - - - -src/routes/utils.ts->src/core/utils/package-json.ts - - - - - -src/routes/utils.ts->src/core/utils/response-handler.ts - - + + - + src/routes/api-config.ts->src/core/utils/logger.ts - - + + - + src/routes/api-config.ts->src/core/database/index.ts - - + + - + src/routes/api-config.ts->src/typings/database.ts - - + + - + src/routes/api-config.ts->src/core/plugins/plugin-manager.ts - - + + - + src/routes/api-config.ts->src/core/utils/package-json.ts - - + + - + src/routes/api-config.ts->src/core/utils/response-handler.ts - - + + - + src/routes/api-config.ts->src/middleware/auth.ts - - + + - + src/routes/docker-manager.ts->src/typings/docker.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-manager.ts->src/core/database/index.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/response-handler.ts - - + + - + src/routes/docker-stats.ts->src/typings/docker.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-stats.ts->src/core/database/index.ts - - + + + + + +src/routes/docker-stats.ts->src/core/utils/helpers.ts + + - + src/routes/docker-stats.ts->src/core/docker/client.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/calculations.ts - - - - - -src/routes/docker-stats.ts->src/core/utils/helpers.ts - - + + - + src/routes/docker-stats.ts->src/typings/dockerode.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/response-handler.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-websocket.ts->src/core/database/index.ts - - + + - + src/routes/docker-websocket.ts->src/core/docker/client.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/response-handler.ts - - + + - + stream - - -stream + + +stream - + src/routes/docker-websocket.ts->stream - - + + - + src/routes/logs.ts->src/core/utils/logger.ts - - + + - + src/routes/logs.ts->src/core/database/index.ts - - + + + + + +src/routes/stacks.ts->src/core/utils/logger.ts + + + + + +src/routes/stacks.ts->src/core/database/index.ts + + + + + +src/routes/stacks.ts->src/core/stacks/controller.ts + + + + + +src/routes/stacks.ts->src/core/utils/response-handler.ts + + + + + +src/routes/utils.ts->src/core/utils/package-json.ts + + + + + +src/routes/utils.ts->src/core/utils/response-handler.ts + + From cbb42fa46483a519225263ff45e4316f187ede37 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 16 Apr 2025 08:17:13 +0200 Subject: [PATCH 239/324] Fix: Formatting --- src/core/stacks/controller.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/core/stacks/controller.ts b/src/core/stacks/controller.ts index 19bd082..221bab4 100644 --- a/src/core/stacks/controller.ts +++ b/src/core/stacks/controller.ts @@ -175,7 +175,6 @@ export async function deployStack( } export async function stopStack(stack_id: number): Promise { - // Note the await to discard the result (convert to void) await runStackCommand( stack_id, (cwd, progressCallback) => @@ -230,8 +229,6 @@ export async function restartStack(stack_id: number): Promise { export async function getStackStatus( stack_id: number ): Promise> { - // Wrap the returned status value to match Promise if that is the expectation. - // In this case, if you need the status, you might adjust the type signature. const status = await runStackCommand( stack_id, async (cwd) => { From a3b0699cd1386824b6fe2be50ede0cf8b5231d62 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 16 Apr 2025 22:14:42 +0200 Subject: [PATCH 240/324] Feat: Linter, formatting and a bunch more --- .github/workflows/pipeline.yaml | 12 + .gitignore | 3 +- .knip.json | 6 +- biome.json | 26 + bun.lock | 21 + package.json | 98 +-- src/core/database/_dbState.ts | 5 + src/core/database/backup.ts | 163 +++++ src/core/database/config.ts | 80 +-- src/core/database/containerStats.ts | 46 +- src/core/database/database.ts | 47 +- src/core/database/dockerHosts.ts | 90 +-- src/core/database/helper.ts | 40 +- src/core/database/hostStats.ts | 38 +- src/core/database/index.ts | 18 +- src/core/database/logs.ts | 108 +-- src/core/database/stacks.ts | 84 +-- src/core/docker/client.ts | 52 +- src/core/docker/monitor.ts | 236 +++--- src/core/docker/scheduler.ts | 180 ++--- src/core/docker/store-container-stats.ts | 166 ++--- src/core/docker/store-host-stats.ts | 132 ++-- src/core/plugins/loader.ts | 98 +-- src/core/plugins/plugin-manager.ts | 228 +++--- src/core/stacks/controller.ts | 555 ++++++++------- src/core/utils/calculations.ts | 50 +- src/core/utils/change-me-checker.ts | 26 +- src/core/utils/helpers.ts | 12 +- src/core/utils/logger.ts | 300 ++++---- src/core/utils/package-json.ts | 24 +- src/core/utils/response-handler.ts | 63 +- src/index.ts | 282 ++++---- src/middleware/auth.ts | 120 ++-- src/plugins/example.plugin.ts | 176 ++--- src/plugins/telegram.plugin.ts | 48 +- src/routes/api-config.ts | 374 ++++++---- src/routes/docker-manager.ts | 218 +++--- src/routes/docker-stats.ts | 294 ++++---- src/routes/docker-websocket.ts | 220 +++--- src/routes/live-logs.ts | 37 +- src/routes/live-stacks.ts | 37 +- src/routes/logs.ts | 174 ++--- src/routes/stacks.ts | 569 ++++++++------- src/routes/utils.ts | 80 +-- src/tests/cleanup.ts | 8 +- src/tests/delete.spec.ts | 12 +- src/tests/gets.spec.ts | 106 +-- src/tests/helper.ts | 215 +++--- src/tests/post.spec.ts | 92 +-- src/typings/database.ts | 34 +- src/typings/docker-compose.ts | 868 +++++++++++++---------- src/typings/docker.ts | 60 +- src/typings/dockerode.ts | 316 ++++----- src/typings/elysiajs.ts | 12 +- src/typings/misc.ts | 5 + src/typings/plugin.ts | 38 +- src/typings/websocket.ts | 18 +- tsconfig.json | 196 ++--- 58 files changed, 4047 insertions(+), 3569 deletions(-) create mode 100644 biome.json create mode 100644 src/core/database/_dbState.ts create mode 100644 src/core/database/backup.ts create mode 100644 src/typings/misc.ts diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index c4ddc1f..4a4f895 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -23,6 +23,18 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Lint + run: | + bun biome check --formatter-enabled=true --linter-enabled=true --organize-imports-enabled=true --reporter=github --fix src + + - name: Commit and Push Changes + uses: EndBug/add-and-commit@v9 + with: + add: "src" + message: "Linting" + committer_name: "GitHub Action" + committer_email: "action@github.com" + - name: Start proxy run: | docker compose -f docker/docker-compose.dev.yaml up -d diff --git a/.gitignore b/.gitignore index 322656b..527c7b3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ /node_modules .test dependency-graph* -build \ No newline at end of file +build +data \ No newline at end of file diff --git a/.knip.json b/.knip.json index 0c1cd54..e786d74 100644 --- a/.knip.json +++ b/.knip.json @@ -1,5 +1,5 @@ { - "entry": ["src/index.ts"], - "project": ["src/**/*.ts"], - "ignore": ["src/plugins/*.plugin.ts","src/tests/*.ts"] + "entry": ["src/index.ts"], + "project": ["src/**/*.ts"], + "ignore": ["src/plugins/*.plugin.ts", "src/tests/*.ts"] } diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..a02c1a3 --- /dev/null +++ b/biome.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": false + }, + "formatter": { + "enabled": true, + "indentStyle": "tab" + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + } +} diff --git a/bun.lock b/bun.lock index ffa2cdf..ad696a2 100644 --- a/bun.lock +++ b/bun.lock @@ -17,6 +17,7 @@ "yaml": "^2.7.1", }, "devDependencies": { + "@biomejs/biome": "1.9.4", "@types/dockerode": "^3.3.38", "@types/node": "^22.14.1", "@types/split2": "^4.2.3", @@ -34,6 +35,24 @@ "packages": { "@balena/dockerignore": ["@balena/dockerignore@1.0.2", "", {}, "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q=="], + "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="], + + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="], + + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="], + + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="], + + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="], + + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="], + + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="], + + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="], + + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], + "@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="], "@dabh/diagnostics": ["@dabh/diagnostics@2.0.3", "", { "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA=="], @@ -368,6 +387,8 @@ "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "color-string/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "defaults/clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], "easy-table/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], diff --git a/package.json b/package.json index b29b98d..8b64f69 100644 --- a/package.json +++ b/package.json @@ -1,51 +1,51 @@ { - "name": "dockstatapi", - "author": { - "email": "info@itsnik.de", - "name": "ItsNik", - "url": "https://github.com/Its4Nik" - }, - "license": "CC BY-NC 4.0", - "contributors": [], - "description": "DockStatAPI is an API backend featuring plugins and more for DockStat", - "version": "3.0.0", - "scripts": { - "start": "cross-env NODE_ENV=production LOG_LEVEL=info bun run src/index.ts", - "start:docker": "bun run build:docker && docker run -p 3000:3000 --rm -d --name dockstatapi -v 'plugins:/DockStatAPI/src/plugins' dockstatapi:local", - "dev": "docker compose -f docker/docker-compose.dev.yaml up -d && cross-env NODE_ENV=dev bun run --watch src/index.ts", - "dev:clean": "bun dev ; echo '\nExiting...' ; bun clean", - "build": "bun build --target bun src/index.ts --outdir ./dist", - "build:docker": "docker build -f docker/Dockerfile . -t 'dockstatapi:local'", - "clean": "bun run clean:win || bun run clean:lin", - "clean:win": "node -e \"process.exit(process.platform === 'win32' ? 0 : 1)\" && cmd /c del /Q dockstatapi.db* && echo 'success'", - "clean:lin": "node -e \"process.exit(process.platform !== 'win32' ? 0 : 1)\" && rm -f dockstatapi.db* && echo 'success'", - "knip": "knip" - }, - "dependencies": { - "@elysiajs/server-timing": "^1.2.1", - "@elysiajs/static": "^1.2.0", - "@elysiajs/swagger": "^1.2.2", - "chalk": "^5.4.1", - "docker-compose": "^1.2.0", - "dockerode": "^4.0.5", - "elysia": "latest", - "knip": "latest", - "split2": "^4.2.0", - "winston": "^3.17.0", - "yaml": "^2.7.1" - }, - "devDependencies": { - "@types/dockerode": "^3.3.38", - "@types/node": "^22.14.1", - "@types/split2": "^4.2.3", - "bun-types": "latest", - "cross-env": "^7.0.3", - "logform": "^2.7.0", - "typescript": "^5.8.3", - "wrap-ansi": "^9.0.0" - }, - "module": "src/index.js", - "trustedDependencies": [ - "protobufjs" - ] + "name": "dockstatapi", + "author": { + "email": "info@itsnik.de", + "name": "ItsNik", + "url": "https://github.com/Its4Nik" + }, + "license": "CC BY-NC 4.0", + "contributors": [], + "description": "DockStatAPI is an API backend featuring plugins and more for DockStat", + "version": "3.0.0", + "scripts": { + "start": "cross-env NODE_ENV=production LOG_LEVEL=info bun run src/index.ts", + "start:docker": "bun run build:docker && docker run -p 3000:3000 --rm -d --name dockstatapi -v 'plugins:/DockStatAPI/src/plugins' dockstatapi:local", + "dev": "docker compose -f docker/docker-compose.dev.yaml up -d && cross-env NODE_ENV=dev bun run --watch src/index.ts", + "dev:clean": "bun dev ; echo '\nExiting...' ; bun clean", + "build": "bun build --target bun src/index.ts --outdir ./dist", + "build:docker": "docker build -f docker/Dockerfile . -t 'dockstatapi:local'", + "clean": "bun run clean:win || bun run clean:lin", + "clean:win": "node -e \"process.exit(process.platform === 'win32' ? 0 : 1)\" && cmd /c del /Q data/dockstatapi*.db* && echo 'success'", + "clean:lin": "node -e \"process.exit(process.platform !== 'win32' ? 0 : 1)\" && rm -f data/dockstatapi*.db* && echo 'success'", + "knip": "knip", + "lint": "biome check --formatter-enabled=true --linter-enabled=true --organize-imports-enabled=true --fix src" + }, + "dependencies": { + "@elysiajs/server-timing": "^1.2.1", + "@elysiajs/static": "^1.2.0", + "@elysiajs/swagger": "^1.2.2", + "chalk": "^5.4.1", + "docker-compose": "^1.2.0", + "dockerode": "^4.0.5", + "elysia": "latest", + "knip": "latest", + "split2": "^4.2.0", + "winston": "^3.17.0", + "yaml": "^2.7.1" + }, + "devDependencies": { + "@biomejs/biome": "1.9.4", + "@types/dockerode": "^3.3.38", + "@types/node": "^22.14.1", + "@types/split2": "^4.2.3", + "bun-types": "latest", + "cross-env": "^7.0.3", + "logform": "^2.7.0", + "typescript": "^5.8.3", + "wrap-ansi": "^9.0.0" + }, + "module": "src/index.js", + "trustedDependencies": ["protobufjs"] } diff --git a/src/core/database/_dbState.ts b/src/core/database/_dbState.ts new file mode 100644 index 0000000..e159ca0 --- /dev/null +++ b/src/core/database/_dbState.ts @@ -0,0 +1,5 @@ +export let backupInProgress = false; + +export function setBackupInProgress(val: boolean) { + backupInProgress = val; +} diff --git a/src/core/database/backup.ts b/src/core/database/backup.ts new file mode 100644 index 0000000..4efa130 --- /dev/null +++ b/src/core/database/backup.ts @@ -0,0 +1,163 @@ +import { copyFileSync, existsSync, readdirSync } from "node:fs"; +import { logger } from "~/core/utils/logger"; +import type { BackupInfo } from "~/typings/misc"; +import { backupInProgress, setBackupInProgress } from "./_dbState"; +import { db } from "./database"; +import { executeDbOperation } from "./helper"; + +export const backupDir = "data/"; + +export async function backupDatabase(): Promise { + if (backupInProgress) { + logger.error("Backup attempt blocked: Another backup already in progress"); + throw new Error("Backup already in progress"); + } + + logger.debug("Starting database backup process..."); + setBackupInProgress(true); + + try { + logger.debug("Executing WAL checkpoint..."); + db.exec("PRAGMA wal_checkpoint(FULL);"); + logger.debug("WAL checkpoint completed successfully"); + + const now = new Date(); + const day = String(now.getDate()).padStart(2, "0"); + const month = String(now.getMonth() + 1).padStart(2, "0"); + const year = now.getFullYear(); + const dateStr = `${day}-${month}-${year}`; + logger.debug(`Using date string for backup: ${dateStr}`); + + logger.debug(`Scanning backup directory: ${backupDir}`); + const files = readdirSync(backupDir); + logger.debug(`Found ${files.length} files in backup directory`); + + const regex = new RegExp( + `^dockstatapi-${day}-${month}-${year}-(\\d+)\\.db\\.bak$`, + ); + let maxBackupNum = 0; + + for (const file of files) { + const match = file.match(regex); + if (match?.[1]) { + const num = Number.parseInt(match[1], 10); + logger.debug(`Found existing backup file: ${file} with number ${num}`); + if (num > maxBackupNum) { + maxBackupNum = num; + } + } else { + logger.debug(`Skipping non-matching file: ${file}`); + } + } + + logger.debug(`Maximum backup number found: ${maxBackupNum}`); + const backupNumber = maxBackupNum + 1; + const backupFilename = `${backupDir}dockstatapi-${dateStr}-${backupNumber}.db.bak`; + logger.debug(`Generated backup filename: ${backupFilename}`); + + logger.debug(`Attempting to copy database to ${backupFilename}`); + try { + copyFileSync(`${backupDir}dockstatapi.db`, backupFilename); + logger.info(`Backup created successfully: ${backupFilename}`); + logger.debug("File copy operation completed without errors"); + } catch (e) { + logger.error(`Failed to create backup file: ${(e as Error).message}`); + throw e; + } + + return backupFilename; + } finally { + setBackupInProgress(false); + logger.debug("Backup process completed, in progress flag reset"); + } +} + +export function restoreDatabase(backupFilename: string): void { + const backupFile = `${backupDir}${backupFilename}`; + + if (backupInProgress) { + logger.error("Restore attempt blocked: Backup in progress"); + throw new Error("Backup in progress. Cannot restore."); + } + + logger.debug(`Starting database restore from ${backupFile}`); + + if (!existsSync(backupFile)) { + logger.error(`Backup file not found: ${backupFile}`); + throw new Error(`Backup file ${backupFile} does not exist.`); + } + + setBackupInProgress(true); + try { + executeDbOperation( + "restore", + () => { + logger.debug(`Attempting to restore database from ${backupFile}`); + try { + copyFileSync(backupFile, `${backupDir}dockstatapi.db`); + logger.info(`Database restored successfully from: ${backupFilename}`); + logger.debug("Database file replacement completed"); + } catch (e) { + logger.error(`Restore failed: ${(e as Error).message}`); + throw e; + } + }, + () => { + if (backupInProgress) { + logger.error("Database operation attempted during restore"); + throw new Error("Cannot perform database operations during restore"); + } + }, + ); + } finally { + setBackupInProgress(false); + logger.debug("Restore process completed, in progress flag reset"); + } +} + +export const findLatestBackup = (): string => { + logger.debug(`Searching for latest backup in directory: ${backupDir}`); + + const files = readdirSync(backupDir); + logger.debug(`Found ${files.length} files to process`); + + const backups = files + .map((file): BackupInfo | null => { + const match = file.match( + /^dockstatapi-(\d{2})-(\d{2})-(\d{4})-(\d+)\.db\.bak$/, + ); + if (!match) { + logger.debug(`Skipping non-backup file: ${file}`); + return null; + } + + const date = new Date( + Number(match[3]), + Number(match[2]) - 1, + Number(match[1]), + ); + logger.debug( + `Found backup file: ${file} with date ${date.toISOString()}`, + ); + + return { + filename: file, + date, + backupNum: Number(match[4]), + }; + }) + .filter((backup): backup is BackupInfo => backup !== null) + .sort((a, b) => { + const dateDiff = b.date.getTime() - a.date.getTime(); + return dateDiff !== 0 ? dateDiff : b.backupNum - a.backupNum; + }); + + if (!backups.length) { + logger.error("No valid backup files found"); + throw new Error("No backups available"); + } + + const latestBackup = backups[0].filename; + logger.debug(`Determined latest backup file: ${latestBackup}`); + return latestBackup; +}; diff --git a/src/core/database/config.ts b/src/core/database/config.ts index 126682e..f2460e0 100644 --- a/src/core/database/config.ts +++ b/src/core/database/config.ts @@ -2,54 +2,54 @@ import { db } from "./database"; import { executeDbOperation } from "./helper"; const stmt = { - update: db.prepare( - `UPDATE config SET fetching_interval = ?, keep_data_for = ?, api_key = ?`, - ), - select: db.prepare( - `SELECT keep_data_for, fetching_interval, api_key FROM config`, - ), - deleteOld: db.prepare( - `DELETE FROM container_stats WHERE timestamp < datetime('now', '-' || ? || ' days')`, - ), - deleteOldLogs: db.prepare( - `DELETE FROM backend_log_entries WHERE timestamp < datetime('now', '-' || ? || ' days')`, - ), + update: db.prepare( + "UPDATE config SET fetching_interval = ?, keep_data_for = ?, api_key = ?", + ), + select: db.prepare( + "SELECT keep_data_for, fetching_interval, api_key FROM config", + ), + deleteOld: db.prepare( + `DELETE FROM container_stats WHERE timestamp < datetime('now', '-' || ? || ' days')`, + ), + deleteOldLogs: db.prepare( + `DELETE FROM backend_log_entries WHERE timestamp < datetime('now', '-' || ? || ' days')`, + ), }; export function updateConfig( - fetching_interval: number, - keep_data_for: number, - api_key: string, + fetching_interval: number, + keep_data_for: number, + api_key: string, ) { - return executeDbOperation( - "Update Config", - () => stmt.update.run(fetching_interval, keep_data_for, api_key), - () => { - if ( - typeof fetching_interval !== "number" || - typeof keep_data_for !== "number" - ) { - throw new TypeError("Invalid config parameters"); - } - }, - ); + return executeDbOperation( + "Update Config", + () => stmt.update.run(fetching_interval, keep_data_for, api_key), + () => { + if ( + typeof fetching_interval !== "number" || + typeof keep_data_for !== "number" + ) { + throw new TypeError("Invalid config parameters"); + } + }, + ); } export function getConfig() { - return executeDbOperation("Get Config", () => stmt.select.all()); + return executeDbOperation("Get Config", () => stmt.select.all()); } export function deleteOldData(days: number) { - return executeDbOperation( - "Delete Old Data", - () => { - db.transaction(() => { - stmt.deleteOld.run(days); - stmt.deleteOldLogs.run(days); - })(); - }, - () => { - if (typeof days !== "number") throw new TypeError("Invalid days type"); - }, - ); + return executeDbOperation( + "Delete Old Data", + () => { + db.transaction(() => { + stmt.deleteOld.run(days); + stmt.deleteOldLogs.run(days); + })(); + }, + () => { + if (typeof days !== "number") throw new TypeError("Invalid days type"); + }, + ); } diff --git a/src/core/database/containerStats.ts b/src/core/database/containerStats.ts index d0fb197..a5d6bcf 100644 --- a/src/core/database/containerStats.ts +++ b/src/core/database/containerStats.ts @@ -7,28 +7,28 @@ const stmt = db.prepare(` `); export function addContainerStats( - id: string, - hostId: string, - name: string, - image: string, - status: string, - state: string, - cpu_usage: number, - memory_usage: number, + id: string, + hostId: string, + name: string, + image: string, + status: string, + state: string, + cpu_usage: number, + memory_usage: number, ) { - return executeDbOperation( - "Add Container Stats", - () => - stmt.run(id, hostId, name, image, status, state, cpu_usage, memory_usage), - () => { - if ( - typeof id !== "string" || - typeof hostId !== "string" || - typeof cpu_usage !== "number" || - typeof memory_usage !== "number" - ) { - throw new TypeError("Invalid container stats parameters"); - } - }, - ); + return executeDbOperation( + "Add Container Stats", + () => + stmt.run(id, hostId, name, image, status, state, cpu_usage, memory_usage), + () => { + if ( + typeof id !== "string" || + typeof hostId !== "string" || + typeof cpu_usage !== "number" || + typeof memory_usage !== "number" + ) { + throw new TypeError("Invalid container stats parameters"); + } + }, + ); } diff --git a/src/core/database/database.ts b/src/core/database/database.ts index 5173e44..db33ec9 100644 --- a/src/core/database/database.ts +++ b/src/core/database/database.ts @@ -1,10 +1,19 @@ import { Database } from "bun:sqlite"; -export const db = new Database("dockstatapi.db", { strict: true }); +import { existsSync, mkdirSync } from "node:fs"; + +const dataFolder = "data"; +if (!existsSync(dataFolder)) { + mkdirSync(dataFolder, { recursive: true }); +} + +export const databasePath = "data/dockstatapi.db"; +export const db = new Database(databasePath, { strict: true }); + db.exec("PRAGMA journal_mode = WAL;"); export function init() { - db.exec(` + db.exec(` CREATE TABLE IF NOT EXISTS backend_log_entries ( timestamp STRING NOT NULL, level TEXT NOT NULL, @@ -57,7 +66,7 @@ export function init() { status TEXT NOT NULL, state TEXT NOT NULL, cpu_usage FLOAT NOT NULL, - memory_usage FLOAT NOT NULL, + memory_usage, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP ); @@ -68,25 +77,25 @@ export function init() { ); `); - const configRow = db - .prepare(`SELECT COUNT(*) AS count FROM config`) - .get() as { count: number }; + const configRow = db + .prepare("SELECT COUNT(*) AS count FROM config") + .get() as { count: number }; - if (configRow.count === 0) { - db.prepare( - `INSERT INTO config (keep_data_for, fetching_interval, api_key) VALUES (7, 5, "changeme")`, - ).run(); - } + if (configRow.count === 0) { + db.prepare( + 'INSERT INTO config (keep_data_for, fetching_interval, api_key) VALUES (7, 5, "changeme")', + ).run(); + } - const hostRow = db - .prepare(`SELECT COUNT(*) AS count FROM docker_hosts`) - .get() as { count: number }; + const hostRow = db + .prepare("SELECT COUNT(*) AS count FROM docker_hosts") + .get() as { count: number }; - if (hostRow.count === 0) { - db.prepare( - `INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)`, - ).run("Localhost", "localhost:2375", false); - } + if (hostRow.count === 0) { + db.prepare( + "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)", + ).run("Localhost", "localhost:2375", false); + } } init(); diff --git a/src/core/database/dockerHosts.ts b/src/core/database/dockerHosts.ts index c805703..18180c5 100644 --- a/src/core/database/dockerHosts.ts +++ b/src/core/database/dockerHosts.ts @@ -1,62 +1,62 @@ +import type { DockerHost } from "~/typings/docker"; import { db } from "./database"; import { executeDbOperation } from "./helper"; -import type { DockerHost } from "~/typings/docker"; const stmt = { - insert: db.prepare( - `INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)`, - ), - selectAll: db.prepare( - `SELECT id, name, hostAddress, secure FROM docker_hosts ORDER BY id DESC`, - ), - update: db.prepare( - `UPDATE docker_hosts SET hostAddress = ?, secure = ?, name = ? WHERE id = ?`, - ), - delete: db.prepare(`DELETE FROM docker_hosts WHERE id = ?`), + insert: db.prepare( + "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)", + ), + selectAll: db.prepare( + "SELECT id, name, hostAddress, secure FROM docker_hosts ORDER BY id DESC", + ), + update: db.prepare( + "UPDATE docker_hosts SET hostAddress = ?, secure = ?, name = ? WHERE id = ?", + ), + delete: db.prepare("DELETE FROM docker_hosts WHERE id = ?"), }; export function addDockerHost(host: DockerHost) { - return executeDbOperation( - "Add Docker Host", - () => stmt.insert.run(host.name, host.hostAddress, host.secure), - () => { - if (!host.name || !host.hostAddress) - throw new Error("Missing required fields"); - if (typeof host.secure !== "boolean") - throw new TypeError("Invalid secure type"); - }, - ); + return executeDbOperation( + "Add Docker Host", + () => stmt.insert.run(host.name, host.hostAddress, host.secure), + () => { + if (!host.name || !host.hostAddress) + throw new Error("Missing required fields"); + if (typeof host.secure !== "boolean") + throw new TypeError("Invalid secure type"); + }, + ); } export function getDockerHosts(): DockerHost[] { - return executeDbOperation("Get Docker Hosts", () => { - const rows = stmt.selectAll.all() as Array< - Omit & { secure: number } - >; - return rows.map((row) => ({ - ...row, - secure: row.secure === 1, - })); - }); + return executeDbOperation("Get Docker Hosts", () => { + const rows = stmt.selectAll.all() as Array< + Omit & { secure: number } + >; + return rows.map((row) => ({ + ...row, + secure: row.secure === 1, + })); + }); } 1; export function updateDockerHost(host: DockerHost) { - return executeDbOperation( - "Update Docker Host", - () => stmt.update.run(host.hostAddress, host.secure, host.name, host.id), - () => { - if (!host.id || typeof host.id !== "number") - throw new Error("Invalid host ID"); - }, - ); + return executeDbOperation( + "Update Docker Host", + () => stmt.update.run(host.hostAddress, host.secure, host.name, host.id), + () => { + if (!host.id || typeof host.id !== "number") + throw new Error("Invalid host ID"); + }, + ); } export function deleteDockerHost(id: number) { - return executeDbOperation( - "Delete Docker Host", - () => stmt.delete.run(id), - () => { - if (typeof id !== "number") throw new TypeError("Invalid ID type"); - }, - ); + return executeDbOperation( + "Delete Docker Host", + () => stmt.delete.run(id), + () => { + if (typeof id !== "number") throw new TypeError("Invalid ID type"); + }, + ); } diff --git a/src/core/database/helper.ts b/src/core/database/helper.ts index 3edbdaa..1f1cabd 100644 --- a/src/core/database/helper.ts +++ b/src/core/database/helper.ts @@ -1,22 +1,28 @@ import { logger } from "~/core/utils/logger"; +import { backupInProgress } from "./_dbState"; export function executeDbOperation( - label: string, - operation: () => T, - validate?: () => void, - dontLog?: boolean, + label: string, + operation: () => T, + validate?: () => void, + dontLog?: boolean, ): T { - const startTime = Date.now(); - if (dontLog !== true) { - logger.debug(`__task__ __db__ ${label} ⏳`); - } - if (validate) { - validate(); - } - const result = operation(); - const duration = Date.now() - startTime; - if (dontLog !== true) { - logger.debug(`__task__ __db__ ${label} ✔️ (${duration}ms)`); - } - return result; + if (backupInProgress && label !== "backup" && label !== "restore") { + throw new Error( + `backup in progress Database operation not allowed: ${label}`, + ); + } + const startTime = Date.now(); + if (dontLog !== true) { + logger.debug(`__task__ __db__ ${label} ⏳`); + } + if (validate) { + validate(); + } + const result = operation(); + const duration = Date.now() - startTime; + if (dontLog !== true) { + logger.debug(`__task__ __db__ ${label} ✔️ (${duration}ms)`); + } + return result; } diff --git a/src/core/database/hostStats.ts b/src/core/database/hostStats.ts index 04aa426..3d48528 100644 --- a/src/core/database/hostStats.ts +++ b/src/core/database/hostStats.ts @@ -1,6 +1,6 @@ +import type { HostStats } from "~/typings/docker"; import { db } from "./database"; import { executeDbOperation } from "./helper"; -import type { HostStats } from "~/typings/docker"; const stmt = db.prepare(` INSERT INTO host_stats ( @@ -24,22 +24,22 @@ const stmt = db.prepare(` `); export function updateHostStats(stats: HostStats) { - return executeDbOperation("Update Host Stats", () => - stmt.run( - stats.hostId, - stats.hostName, - stats.dockerVersion, - stats.apiVersion, - stats.os, - stats.architecture, - stats.totalMemory, - stats.totalCPU, - JSON.stringify(stats.labels), - stats.containers, - stats.containersRunning, - stats.containersStopped, - stats.containersPaused, - stats.images, - ), - ); + return executeDbOperation("Update Host Stats", () => + stmt.run( + stats.hostId, + stats.hostName, + stats.dockerVersion, + stats.apiVersion, + stats.os, + stats.architecture, + stats.totalMemory, + stats.totalCPU, + JSON.stringify(stats.labels), + stats.containers, + stats.containersRunning, + stats.containersStopped, + stats.containersPaused, + stats.images, + ), + ); } diff --git a/src/core/database/index.ts b/src/core/database/index.ts index 3559fe9..9158cad 100644 --- a/src/core/database/index.ts +++ b/src/core/database/index.ts @@ -2,18 +2,20 @@ import { init } from "~/core/database/database"; init(); -import * as dockerHosts from "~/core/database/dockerHosts"; -import * as logs from "~/core/database/logs"; +import * as backup from "~/core/database/backup"; import * as config from "~/core/database/config"; import * as containerStats from "~/core/database/containerStats"; +import * as dockerHosts from "~/core/database/dockerHosts"; import * as hostStats from "~/core/database/hostStats"; +import * as logs from "~/core/database/logs"; import * as stacks from "~/core/database/stacks"; export const dbFunctions = { - ...dockerHosts, - ...logs, - ...config, - ...containerStats, - ...hostStats, - ...stacks, + ...dockerHosts, + ...logs, + ...config, + ...containerStats, + ...hostStats, + ...stacks, + ...backup, }; diff --git a/src/core/database/logs.ts b/src/core/database/logs.ts index 26ce2c1..3fba8dc 100644 --- a/src/core/database/logs.ts +++ b/src/core/database/logs.ts @@ -1,73 +1,73 @@ -import { logStreamData } from "~/typings/websocket"; +import type { log_message } from "~/typings/database"; import { db } from "./database"; import { executeDbOperation } from "./helper"; const stmt = { - insert: db.prepare( - `INSERT INTO backend_log_entries (timestamp, level, message, file, line) VALUES (?, ?, ?, ?, ?)`, - ), - selectAll: db.prepare( - `SELECT timestamp, level, message, file, line FROM backend_log_entries ORDER BY timestamp DESC`, - ), - selectByLevel: db.prepare( - `SELECT timestamp, level, message, file, line FROM backend_log_entries WHERE level = ?`, - ), - deleteAll: db.prepare(`DELETE FROM backend_log_entries`), - deleteByLevel: db.prepare(`DELETE FROM backend_log_entries WHERE level = ?`), + insert: db.prepare( + "INSERT INTO backend_log_entries (timestamp, level, message, file, line) VALUES (?, ?, ?, ?, ?)", + ), + selectAll: db.prepare( + "SELECT timestamp, level, message, file, line FROM backend_log_entries ORDER BY timestamp DESC", + ), + selectByLevel: db.prepare( + "SELECT timestamp, level, message, file, line FROM backend_log_entries WHERE level = ?", + ), + deleteAll: db.prepare("DELETE FROM backend_log_entries"), + deleteByLevel: db.prepare("DELETE FROM backend_log_entries WHERE level = ?"), }; -export function addLogEntry(data: logStreamData) { - return executeDbOperation( - "Add Log Entry", - () => - stmt.insert.run( - data.level, - data.timestamp, - data.message, - data.file, - data.line, - ), - () => { - if ( - typeof data.level !== "string" || - typeof data.timestamp !== "string" || - typeof data.message !== "string" || - typeof data.file !== "string" || - typeof data.line !== "number" - ) { - throw new TypeError( - `Invalid log entry parameters ${data.file} ${data.line} ${data.message} ${data}`, - ); - } - }, - true, - ); +export function addLogEntry(data: log_message) { + return executeDbOperation( + "Add Log Entry", + () => + stmt.insert.run( + data.level, + data.timestamp, + data.message, + data.file, + data.line, + ), + () => { + if ( + typeof data.level !== "string" || + typeof data.timestamp !== "string" || + typeof data.message !== "string" || + typeof data.file !== "string" || + typeof data.line !== "number" + ) { + throw new TypeError( + "Invalid log entry parameters ${data.file} ${data.line} ${data.message} ${data}", + ); + } + }, + true, + ); } export function getAllLogs() { - return executeDbOperation("Get All Logs", () => stmt.selectAll.all()); + return executeDbOperation("Get All Logs", () => stmt.selectAll.all()); } export function getLogsByLevel(level: string) { - return executeDbOperation( - "Get Logs By Level", - () => stmt.selectByLevel.all(level), - () => { - if (typeof level !== "string") throw new TypeError("Invalid level type"); - }, - ); + return executeDbOperation( + "Get Logs By Level", + () => stmt.selectByLevel.all(level), + () => { + if (typeof level !== "string") throw new TypeError("Invalid level type"); + }, + ); } export function clearAllLogs() { - return executeDbOperation("Clear All Logs", () => stmt.deleteAll.run()); + return executeDbOperation("Clear All Logs", () => stmt.deleteAll.run()); } export function clearLogsByLevel(level: string) { - return executeDbOperation( - "Clear Logs By Level", - () => stmt.deleteByLevel.run(level), - () => { - if (typeof level !== "string") throw new TypeError("Invalid level type"); - }, - ); + return executeDbOperation( + "Clear Logs By Level", + () => stmt.deleteByLevel.run(level), + () => { + if (typeof level !== "string") throw new TypeError("Invalid level type"); + }, + ); } diff --git a/src/core/database/stacks.ts b/src/core/database/stacks.ts index 18aa116..f39d01a 100644 --- a/src/core/database/stacks.ts +++ b/src/core/database/stacks.ts @@ -1,75 +1,75 @@ -import { Stack } from "~/typings/docker-compose"; -import { db } from "./database"; -import { executeDbOperation } from "./helper"; import type { stacks_config } from "~/typings/database"; +import type { Stack } from "~/typings/docker-compose"; import { findObjectByKey } from "../utils/helpers"; +import { db } from "./database"; +import { executeDbOperation } from "./helper"; const stmt = { - insert: db.prepare(` + insert: db.prepare(` INSERT INTO stacks_config ( name, version, custom, source, container_count, stack_prefix, automatic_reboot_on_error, image_updates ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `), - selectAll: db.prepare(` + selectAll: db.prepare(` SELECT id, name, version, custom, source, container_count, stack_prefix, automatic_reboot_on_error, image_updates FROM stacks_config ORDER BY name DESC `), - update: db.prepare(` + update: db.prepare(` UPDATE stacks_config SET version = ?, custom = ?, source = ?, container_count = ?, stack_prefix = ?, automatic_reboot_on_error = ?, image_updates = ? WHERE name = ? `), - delete: db.prepare(`DELETE FROM stacks_config WHERE id = ?`), + delete: db.prepare("DELETE FROM stacks_config WHERE id = ?"), }; export function addStack(stack: stacks_config) { - executeDbOperation("Add Stack", () => - stmt.insert.run( - stack.name, - stack.version, - stack.custom, - stack.source, - stack.container_count, - stack.stack_prefix, - stack.automatic_reboot_on_error, - stack.image_updates - ) - ); + executeDbOperation("Add Stack", () => + stmt.insert.run( + stack.name, + stack.version, + stack.custom, + stack.source, + stack.container_count, + stack.stack_prefix, + stack.automatic_reboot_on_error, + stack.image_updates, + ), + ); - return findObjectByKey(getStacks(), "name", stack.name)?.id; + return findObjectByKey(getStacks(), "name", stack.name)?.id; } export function getStacks() { - return executeDbOperation("Get Stacks", () => - stmt.selectAll.all() - ) as Stack[]; + return executeDbOperation("Get Stacks", () => + stmt.selectAll.all(), + ) as Stack[]; } export function deleteStack(id: number) { - return executeDbOperation( - "Delete Stack", - () => stmt.delete.run(id), - () => { - if (typeof id !== "number") throw new TypeError("Invalid stack ID"); - } - ); + return executeDbOperation( + "Delete Stack", + () => stmt.delete.run(id), + () => { + if (typeof id !== "number") throw new TypeError("Invalid stack ID"); + }, + ); } export function updateStack(stack: stacks_config) { - return executeDbOperation("Update Stack", () => - stmt.update.run( - stack.version, - stack.custom, - stack.source, - stack.container_count, - stack.stack_prefix, - stack.automatic_reboot_on_error, - stack.image_updates, - stack.name - ) - ); + return executeDbOperation("Update Stack", () => + stmt.update.run( + stack.version, + stack.custom, + stack.source, + stack.container_count, + stack.stack_prefix, + stack.automatic_reboot_on_error, + stack.image_updates, + stack.name, + ), + ); } diff --git a/src/core/docker/client.ts b/src/core/docker/client.ts index 7d8e6ea..ad65540 100644 --- a/src/core/docker/client.ts +++ b/src/core/docker/client.ts @@ -1,33 +1,33 @@ -import type { DockerHost } from "~/typings/docker"; import Docker from "dockerode"; import { logger } from "~/core/utils/logger"; +import type { DockerHost } from "~/typings/docker"; export const getDockerClient = (host: DockerHost): Docker => { - try { - const inputUrl = host.hostAddress.includes("://") - ? host.hostAddress - : `${host.secure ? "https" : "http"}://${host.hostAddress}`; - const parsedUrl = new URL(inputUrl); - const hostAddress = parsedUrl.hostname; - let port = parsedUrl.port - ? parseInt(parsedUrl.port) - : host.secure - ? 2376 - : 2375; + try { + const inputUrl = host.hostAddress.includes("://") + ? host.hostAddress + : `${host.secure ? "https" : "http"}://${host.hostAddress}`; + const parsedUrl = new URL(inputUrl); + const hostAddress = parsedUrl.hostname; + const port = parsedUrl.port + ? Number.parseInt(parsedUrl.port) + : host.secure + ? 2376 + : 2375; - if (isNaN(port) || port < 1 || port > 65535) { - throw new Error("Invalid port number in Docker host URL"); - } + if (Number.isNaN(port) || port < 1 || port > 65535) { + throw new Error("Invalid port number in Docker host URL"); + } - return new Docker({ - protocol: host.secure ? "https" : "http", - host: hostAddress, - port, - version: "v1.41", - // TODO: Add TLS configuration if needed - }); - } catch (error) { - logger.error("Invalid Docker host URL configuration:", error); - throw new Error("Invalid Docker host configuration"); - } + return new Docker({ + protocol: host.secure ? "https" : "http", + host: hostAddress, + port, + version: "v1.41", + // TODO: Add TLS configuration if needed + }); + } catch (error) { + logger.error("Invalid Docker host URL configuration:", error); + throw new Error("Invalid Docker host configuration"); + } }; diff --git a/src/core/docker/monitor.ts b/src/core/docker/monitor.ts index eadd5f6..d10c3c6 100644 --- a/src/core/docker/monitor.ts +++ b/src/core/docker/monitor.ts @@ -1,138 +1,142 @@ -import type { DockerHost } from "~/typings/docker"; +import { sleep } from "bun"; +import Docker from "dockerode"; import { dbFunctions } from "~/core/database"; import { getDockerClient } from "~/core/docker/client"; import { logger } from "~/core/utils/logger"; +import type { DockerHost } from "~/typings/docker"; +import type { ContainerInfo } from "~/typings/docker"; import { pluginManager } from "../plugins/plugin-manager"; -import { ContainerInfo } from "~/typings/docker"; -import { sleep } from "bun"; export async function monitorDockerEvents() { - let hosts: DockerHost[]; + let hosts: DockerHost[]; - try { - hosts = dbFunctions.getDockerHosts(); - logger.debug( - `Retrieved ${hosts.length} Docker host(s) for event monitoring.`, - ); - } catch (error: unknown) { - logger.error(`Error retrieving Docker hosts: ${(error as Error).message}`); - return; - } + try { + hosts = dbFunctions.getDockerHosts(); + logger.debug( + `Retrieved ${hosts.length} Docker host(s) for event monitoring.`, + ); + } catch (error: unknown) { + logger.error(`Error retrieving Docker hosts: ${(error as Error).message}`); + return; + } - for (const host of hosts) { - await startFor(host); - } + for (const host of hosts) { + await startFor(host); + } } async function startFor(host: DockerHost) { - const docker = getDockerClient(host); - try { - await docker.ping(); - pluginManager.handleHostReachableAgain(host.name); - } catch (err: any) { - logger.warn(`Restarting Stream for ${host.name} in 10 seconds...`); - pluginManager.handleHostUnreachable(host.name, err); - await sleep(10000); - startFor(host); - } + const docker = getDockerClient(host); + try { + await docker.ping(); + pluginManager.handleHostReachableAgain(host.name); + } catch (err) { + logger.warn(`Restarting Stream for ${host.name} in 10 seconds...`); + pluginManager.handleHostUnreachable(host.name, String(err)); + await sleep(10000); + startFor(host); + } - try { - const eventsStream = await docker.getEvents(); - logger.debug(`Started events stream for host: ${host.name}`); + try { + const eventsStream = await docker.getEvents(); + logger.debug(`Started events stream for host: ${host.name}`); - let buffer = ""; + let buffer = ""; - eventsStream.on("data", (chunk: Buffer) => { - buffer += chunk.toString("utf8"); - const lines = buffer.split(/\r?\n/); + eventsStream.on("data", (chunk: Buffer) => { + buffer += chunk.toString("utf8"); + const lines = buffer.split(/\r?\n/); - buffer = lines.pop() || ""; + buffer = lines.pop() || ""; - for (const line of lines) { - if (line.trim() === "") { - continue; - } + for (const line of lines) { + if (line.trim() === "") { + continue; + } - let event: any; - try { - event = JSON.parse(line); - } catch (parseErr: any) { - logger.error( - `Failed to parse event from host ${host.name}: ${parseErr.message}`, - ); - continue; - } + //biome-ignore lint/suspicious/noExplicitAny: Unsure what data we are receiving here + let event: any; + try { + event = JSON.parse(line); + } catch (parseErr) { + logger.error( + `Failed to parse event from host ${host.name}: ${String(parseErr)}`, + ); + continue; + } - if (event.Type === "container") { - const containerInfo: ContainerInfo = { - id: event.Actor?.ID || event.id || "", - hostId: host.name, - name: event.Actor?.Attributes?.name || "", - image: event.Actor?.Attributes?.image || event.from || "", - status: event.status || event.Actor?.Attributes?.status || "", - state: event.Actor?.Attributes?.state || event.Action || "", - cpuUsage: 0, - memoryUsage: 0, - }; + if (event.Type === "container") { + const containerInfo: ContainerInfo = { + id: event.Actor?.ID || event.id || "", + hostId: host.name, + name: event.Actor?.Attributes?.name || "", + image: event.Actor?.Attributes?.image || event.from || "", + status: event.status || event.Actor?.Attributes?.status || "", + state: event.Actor?.Attributes?.state || event.Action || "", + cpuUsage: 0, + memoryUsage: 0, + }; - const action = event.Action; - logger.debug(`Triggering Action [${action}]`); - switch (action) { - case "stop": - pluginManager.handleContainerStop(containerInfo); - break; - case "start": - pluginManager.handleContainerStart(containerInfo); - break; - case "die": - pluginManager.handleContainerDie(containerInfo); - break; - case "kill": - pluginManager.handleContainerKill(containerInfo); - break; - case "create": - pluginManager.handleContainerCreate(containerInfo); - break; - case "destroy": - pluginManager.handleContainerDestroy(containerInfo); - break; - case "pause": - pluginManager.handleContainerPause(containerInfo); - break; - case "unpause": - pluginManager.handleContainerUnpause(containerInfo); - break; - case "restart": - pluginManager.handleContainerRestart(containerInfo); - break; - case "update": - pluginManager.handleContainerUpdate(containerInfo); - break; - case "health_status": - pluginManager.handleContainerHealthStatus(containerInfo); - break; - default: - logger.debug( - `Unhandled container event "${action}" on host ${host.name}`, - ); - } - } - } - }); + const action = event.Action; + logger.debug(`Triggering Action [${action}]`); + switch (action) { + case "stop": + pluginManager.handleContainerStop(containerInfo); + break; + case "start": + pluginManager.handleContainerStart(containerInfo); + break; + case "die": + pluginManager.handleContainerDie(containerInfo); + break; + case "kill": + pluginManager.handleContainerKill(containerInfo); + break; + case "create": + pluginManager.handleContainerCreate(containerInfo); + break; + case "destroy": + pluginManager.handleContainerDestroy(containerInfo); + break; + case "pause": + pluginManager.handleContainerPause(containerInfo); + break; + case "unpause": + pluginManager.handleContainerUnpause(containerInfo); + break; + case "restart": + pluginManager.handleContainerRestart(containerInfo); + break; + case "update": + pluginManager.handleContainerUpdate(containerInfo); + break; + case "health_status": + pluginManager.handleContainerHealthStatus(containerInfo); + break; + default: + logger.debug( + `Unhandled container event "${action}" on host ${host.name}`, + ); + } + } + } + }); - eventsStream.on("error", async (err: Error) => { - logger.error(`Events stream error for host ${host.name}: ${err.message}`); - logger.warn(`Restarting Stream for ${host.name} in 10 seconds...`); - await sleep(10000); - startFor(host); - }); + eventsStream.on("error", async (err: Error) => { + logger.error(`Events stream error for host ${host.name}: ${err.message}`); + logger.warn(`Restarting Stream for ${host.name} in 10 seconds...`); + await sleep(10000); + startFor(host); + }); - eventsStream.on("end", () => { - logger.info(`Events stream ended for host ${host.name}`); - }); - } catch (streamErr: any) { - logger.error( - `Failed to start events stream for host ${host.name}: ${streamErr.message}`, - ); - } + eventsStream.on("end", () => { + logger.info(`Events stream ended for host ${host.name}`); + }); + } catch (streamErr) { + logger.error( + `Failed to start events stream for host ${host.name}: ${String( + streamErr, + )}`, + ); + } } diff --git a/src/core/docker/scheduler.ts b/src/core/docker/scheduler.ts index d1dd124..8682411 100644 --- a/src/core/docker/scheduler.ts +++ b/src/core/docker/scheduler.ts @@ -1,115 +1,115 @@ -import storeContainerData from "~/core/docker/store-container-stats"; import { dbFunctions } from "~/core/database"; -import { config } from "~/typings/database"; -import { logger } from "~/core/utils/logger"; +import storeContainerData from "~/core/docker/store-container-stats"; import storeHostData from "~/core/docker/store-host-stats"; +import { logger } from "~/core/utils/logger"; +import type { config } from "~/typings/database"; function convertFromMinToMs(minutes: number): number { - return minutes * 60 * 1000; + return minutes * 60 * 1000; } async function initialRun( - scheduleName: string, - scheduleFunction: Promise | void, - isAsync: boolean + scheduleName: string, + scheduleFunction: Promise | void, + isAsync: boolean, ) { - try { - if (isAsync) { - await scheduleFunction; - } else { - scheduleFunction; - } - logger.info(`Startup run success for: ${scheduleName}`); - } catch (error) { - logger.error(`Startup run failed for ${scheduleName}, ${error as string}`); - } + try { + if (isAsync) { + await scheduleFunction; + } else { + scheduleFunction; + } + logger.info(`Startup run success for: ${scheduleName}`); + } catch (error) { + logger.error(`Startup run failed for ${scheduleName}, ${error as string}`); + } } async function setSchedules() { - try { - const rawConfigData: unknown[] = dbFunctions.getConfig(); - const configData = rawConfigData[0]; + try { + const rawConfigData: unknown[] = dbFunctions.getConfig(); + const configData = rawConfigData[0]; - if ( - !configData || - typeof (configData as config).keep_data_for !== "number" || - typeof (configData as config).fetching_interval !== "number" - ) { - logger.error("Invalid configuration data:", configData); - throw new Error("Invalid configuration data"); - } + if ( + !configData || + typeof (configData as config).keep_data_for !== "number" || + typeof (configData as config).fetching_interval !== "number" + ) { + logger.error("Invalid configuration data:", configData); + throw new Error("Invalid configuration data"); + } - const { keep_data_for, fetching_interval } = configData as config; + const { keep_data_for, fetching_interval } = configData as config; - if (keep_data_for === undefined) { - const errMsg = "keep_data_for is undefined"; - logger.error(errMsg); - throw new Error(errMsg); - } + if (keep_data_for === undefined) { + const errMsg = "keep_data_for is undefined"; + logger.error(errMsg); + throw new Error(errMsg); + } - if (fetching_interval === undefined) { - const errMsg = "fetching_interval is undefined"; - logger.error(errMsg); - throw new Error(errMsg); - } + if (fetching_interval === undefined) { + const errMsg = "fetching_interval is undefined"; + logger.error(errMsg); + throw new Error(errMsg); + } - logger.info( - `Scheduling: Fetching container statistics every ${fetching_interval} minutes` - ); + logger.info( + `Scheduling: Fetching container statistics every ${fetching_interval} minutes`, + ); - logger.info( - `Scheduling: Updating host statistics every ${fetching_interval} minutes` - ); + logger.info( + `Scheduling: Updating host statistics every ${fetching_interval} minutes`, + ); - logger.info( - `Scheduling: Cleaning up Database every hour and deleting data older then ${keep_data_for} days` - ); + logger.info( + `Scheduling: Cleaning up Database every hour and deleting data older then ${keep_data_for} days`, + ); - // Schedule container data fetching - await initialRun("storeContainerData", storeContainerData(), true); - setInterval(async () => { - try { - logger.info("Task Start: Fetching container data."); - await storeContainerData(); - logger.info("Task End: Container data fetched successfully."); - } catch (error) { - logger.error("Error in fetching container data:", error); - } - }, convertFromMinToMs(fetching_interval)); + // Schedule container data fetching + await initialRun("storeContainerData", storeContainerData(), true); + setInterval(async () => { + try { + logger.info("Task Start: Fetching container data."); + await storeContainerData(); + logger.info("Task End: Container data fetched successfully."); + } catch (error) { + logger.error("Error in fetching container data:", error); + } + }, convertFromMinToMs(fetching_interval)); - // Schedule Host statistics updates - await initialRun("storeHostData", storeHostData(), true); - setInterval(async () => { - try { - logger.info("Task Start: Updating host stats."); - await storeHostData(); - logger.info("Task End: Updating host stats successfully."); - } catch (error) { - logger.error("Error in updating host stats:", error); - } - }, convertFromMinToMs(fetching_interval)); + // Schedule Host statistics updates + await initialRun("storeHostData", storeHostData(), true); + setInterval(async () => { + try { + logger.info("Task Start: Updating host stats."); + await storeHostData(); + logger.info("Task End: Updating host stats successfully."); + } catch (error) { + logger.error("Error in updating host stats:", error); + } + }, convertFromMinToMs(fetching_interval)); - // Schedule database cleanup - await initialRun( - "dbFunctions.deleteOldData", - dbFunctions.deleteOldData(keep_data_for), - false - ); - setInterval(() => { - try { - logger.info("Task Start: Cleaning up old database data."); - dbFunctions.deleteOldData(keep_data_for); - logger.info("Task End: Database cleanup completed."); - } catch (error) { - logger.error("Error in database cleanup task:", error); - } - }, convertFromMinToMs(60)); + // Schedule database cleanup + await initialRun( + "dbFunctions.deleteOldData", + dbFunctions.deleteOldData(keep_data_for), + false, + ); + setInterval(() => { + try { + logger.info("Task Start: Cleaning up old database data."); + dbFunctions.deleteOldData(keep_data_for); + logger.info("Task End: Database cleanup completed."); + } catch (error) { + logger.error("Error in database cleanup task:", error); + } + }, convertFromMinToMs(60)); - logger.info("Schedules have been set successfully."); - } catch (error) { - logger.error("Error setting schedules:", error); - throw error; - } + logger.info("Schedules have been set successfully."); + } catch (error) { + logger.error("Error setting schedules:", error); + throw error; + } } export { setSchedules }; diff --git a/src/core/docker/store-container-stats.ts b/src/core/docker/store-container-stats.ts index 69c12e0..97e0bd9 100644 --- a/src/core/docker/store-container-stats.ts +++ b/src/core/docker/store-container-stats.ts @@ -1,98 +1,98 @@ -import { getDockerClient } from "~/core/docker/client"; +import type Docker from "dockerode"; import { dbFunctions } from "~/core/database"; -import Docker from "dockerode"; +import { getDockerClient } from "~/core/docker/client"; import { - calculateCpuPercent, - calculateMemoryUsage, + calculateCpuPercent, + calculateMemoryUsage, } from "~/core/utils/calculations"; import { logger } from "../utils/logger"; async function storeContainerData() { - try { - const hosts = dbFunctions.getDockerHosts(); - logger.debug("Retrieved docker hosts for storring container data"); + try { + const hosts = dbFunctions.getDockerHosts(); + logger.debug("Retrieved docker hosts for storring container data"); - // Process each host concurrently and wait for them all to finish - await Promise.all( - hosts.map(async (host) => { - const docker = getDockerClient(host); + // Process each host concurrently and wait for them all to finish + await Promise.all( + hosts.map(async (host) => { + const docker = getDockerClient(host); - // Test the connection with a ping - try { - await docker.ping(); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error( - `Failed to ping docker host "${host.name}": ${errMsg}`, - ); - } + // Test the connection with a ping + try { + await docker.ping(); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error( + `Failed to ping docker host "${host.name}": ${errMsg}`, + ); + } - let containers; - try { - containers = await docker.listContainers({ all: true }); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error( - `Failed to list containers on host "${host.name}": ${errMsg}`, - ); - } + let containers: Docker.ContainerInfo[] = []; + try { + containers = await docker.listContainers({ all: true }); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error( + `Failed to list containers on host "${host.name}": ${errMsg}`, + ); + } - // Process each container concurrently - await Promise.all( - containers.map(async (containerInfo) => { - const containerName = containerInfo.Names[0].replace(/^\//, ""); - try { - const container = docker.getContainer(containerInfo.Id); + // Process each container concurrently + await Promise.all( + containers.map(async (containerInfo) => { + const containerName = containerInfo.Names[0].replace(/^\//, ""); + try { + const container = docker.getContainer(containerInfo.Id); - const stats: Docker.ContainerStats = await new Promise( - (resolve, reject) => { - container.stats({ stream: false }, (error, stats) => { - if (error) { - const errMsg = - error instanceof Error ? error.message : String(error); - return reject( - new Error( - `Failed to get stats for container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}": ${errMsg}`, - ), - ); - } - if (!stats) { - return reject( - new Error( - `No stats returned for container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}".`, - ), - ); - } - resolve(stats); - }); - }, - ); + const stats: Docker.ContainerStats = await new Promise( + (resolve, reject) => { + container.stats({ stream: false }, (error, stats) => { + if (error) { + const errMsg = + error instanceof Error ? error.message : String(error); + return reject( + new Error( + `Failed to get stats for container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}": ${errMsg}`, + ), + ); + } + if (!stats) { + return reject( + new Error( + `No stats returned for container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}".`, + ), + ); + } + resolve(stats); + }); + }, + ); - dbFunctions.addContainerStats( - containerInfo.Id, - host.name, - containerName, - containerInfo.Image, - containerInfo.Status, - containerInfo.State, - calculateCpuPercent(stats), - calculateMemoryUsage(stats), - ); - } catch (error) { - const errMsg = - error instanceof Error ? error.message : String(error); - throw new Error( - `Error processing container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}": ${errMsg}`, - ); - } - }), - ); - }), - ); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(`Failed to store container data: ${errMsg}`); - } + dbFunctions.addContainerStats( + containerInfo.Id, + host.name, + containerName, + containerInfo.Image, + containerInfo.Status, + containerInfo.State, + calculateCpuPercent(stats), + calculateMemoryUsage(stats), + ); + } catch (error) { + const errMsg = + error instanceof Error ? error.message : String(error); + throw new Error( + `Error processing container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}": ${errMsg}`, + ); + } + }), + ); + }), + ); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to store container data: ${errMsg}`); + } } export default storeContainerData; diff --git a/src/core/docker/store-host-stats.ts b/src/core/docker/store-host-stats.ts index fa7b05d..053f37e 100644 --- a/src/core/docker/store-host-stats.ts +++ b/src/core/docker/store-host-stats.ts @@ -1,84 +1,84 @@ -import { logger } from "~/core/utils/logger"; import { dbFunctions } from "~/core/database"; -import { DockerHost, HostStats } from "~/typings/docker"; import { getDockerClient } from "~/core/docker/client"; -import { DockerInfo } from "~/typings/dockerode"; import { findObjectByKey } from "~/core/utils/helpers"; +import { logger } from "~/core/utils/logger"; +import type { DockerHost, HostStats } from "~/typings/docker"; +import type { DockerInfo } from "~/typings/dockerode"; function getHostByName(hostName: string): DockerHost { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const foundHost = findObjectByKey(hosts, "name", hostName); - if (!foundHost) { - throw new Error(`Host ${hostName} not found`); - } - return foundHost; + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + const foundHost = findObjectByKey(hosts, "name", hostName); + if (!foundHost) { + throw new Error(`Host ${hostName} not found`); + } + return foundHost; } async function storeHostData() { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - await Promise.all( - hosts.map(async (host) => { - const docker = getDockerClient(host); + await Promise.all( + hosts.map(async (host) => { + const docker = getDockerClient(host); - try { - await docker.ping(); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error( - `Failed to ping docker host "${host.name}": ${errMsg}`, - ); - } + try { + await docker.ping(); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error( + `Failed to ping docker host "${host.name}": ${errMsg}`, + ); + } - let hostStats: DockerInfo; - try { - hostStats = await docker.info(); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error( - `Failed to fetch stats for host "${host.name}": ${errMsg}`, - ); - } + let hostStats: DockerInfo; + try { + hostStats = await docker.info(); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error( + `Failed to fetch stats for host "${host.name}": ${errMsg}`, + ); + } - const hostId = getHostByName(host.name).id; + const hostId = getHostByName(host.name).id; - if (!hostId) { - throw new Error(`Host "${host.name}" not found`); - } + if (!hostId) { + throw new Error(`Host "${host.name}" not found`); + } - try { - const stats: HostStats = { - hostId: hostId, - hostName: host.name, - dockerVersion: hostStats.ServerVersion, - apiVersion: hostStats.Driver, - os: hostStats.OperatingSystem, - architecture: hostStats.Architecture, - totalMemory: hostStats.MemTotal, - totalCPU: hostStats.NCPU, - labels: hostStats.Labels, - images: hostStats.Images, - containers: hostStats.Containers, - containersPaused: hostStats.ContainersPaused, - containersRunning: hostStats.ContainersRunning, - containersStopped: hostStats.ContainersStopped, - }; + try { + const stats: HostStats = { + hostId: hostId, + hostName: host.name, + dockerVersion: hostStats.ServerVersion, + apiVersion: hostStats.Driver, + os: hostStats.OperatingSystem, + architecture: hostStats.Architecture, + totalMemory: hostStats.MemTotal, + totalCPU: hostStats.NCPU, + labels: hostStats.Labels, + images: hostStats.Images, + containers: hostStats.Containers, + containersPaused: hostStats.ContainersPaused, + containersRunning: hostStats.ContainersRunning, + containersStopped: hostStats.ContainersStopped, + }; - dbFunctions.updateHostStats(stats); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error( - `Failed to store stats for host "${host.name}": ${errMsg}`, - ); - } - }), - ); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - logger.error(`storeHostData failed: ${errMsg}`); - throw new Error(`Failed to store host data: ${errMsg}`); - } + dbFunctions.updateHostStats(stats); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error( + `Failed to store stats for host "${host.name}": ${errMsg}`, + ); + } + }), + ); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + logger.error(`storeHostData failed: ${errMsg}`); + throw new Error(`Failed to store host data: ${errMsg}`); + } } export default storeHostData; diff --git a/src/core/plugins/loader.ts b/src/core/plugins/loader.ts index 6fad398..854bc5a 100644 --- a/src/core/plugins/loader.ts +++ b/src/core/plugins/loader.ts @@ -1,53 +1,53 @@ -import { pluginManager } from "./plugin-manager"; -import path from "path"; -import fs from "fs"; -import { logger } from "../utils/logger"; +import fs from "node:fs"; +import path from "node:path"; import { checkFileForChangeMe } from "../utils/change-me-checker"; +import { logger } from "../utils/logger"; +import { pluginManager } from "./plugin-manager"; export async function loadPlugins(pluginDir: string) { - const pluginPath = path.join(process.cwd(), pluginDir); - - logger.debug(`Loading plugins (${pluginPath})`); - - if (!fs.existsSync(pluginPath)) { - throw new Error(`Failed to check plugin directory`); - } - logger.debug(`Plugin directory exists`); - - let pluginCount = 0; - let files; - try { - files = fs.readdirSync(pluginPath); - logger.debug(`Found ${files.length} files in plugin directory`); - } catch (error) { - throw new Error(`Failed to read plugin-directory: ${error}`); - } - - if (!files) { - logger.info(`No plugins found in ${pluginPath}`); - return; - } - - for (const file of files) { - if (!file.endsWith(".plugin.ts")) { - logger.debug(`Skipping non-plugin file: ${file}`); - continue; - } - - const absolutePath = path.join(pluginPath, file); - logger.info(`Loading plugin: ${absolutePath}`); - try { - await checkFileForChangeMe(absolutePath); - const module = await import(absolutePath); - const plugin = module.default; - pluginManager.register(plugin); - pluginCount++; - } catch (error) { - logger.error( - `Error while importing plugin ${absolutePath}: ${error as string}`, - ); - } - } - - logger.info(`Registered ${pluginCount} plugin(s)`); + const pluginPath = path.join(process.cwd(), pluginDir); + + logger.debug(`Loading plugins (${pluginPath})`); + + if (!fs.existsSync(pluginPath)) { + throw new Error("Failed to check plugin directory"); + } + logger.debug("Plugin directory exists"); + + let pluginCount = 0; + let files: string[]; + try { + files = fs.readdirSync(pluginPath); + logger.debug(`Found ${files.length} files in plugin directory`); + } catch (error) { + throw new Error(`Failed to read plugin-directory: ${error}`); + } + + if (!files) { + logger.info(`No plugins found in ${pluginPath}`); + return; + } + + for (const file of files) { + if (!file.endsWith(".plugin.ts")) { + logger.debug(`Skipping non-plugin file: ${file}`); + continue; + } + + const absolutePath = path.join(pluginPath, file); + logger.info(`Loading plugin: ${absolutePath}`); + try { + await checkFileForChangeMe(absolutePath); + const module = await import(absolutePath); + const plugin = module.default; + pluginManager.register(plugin); + pluginCount++; + } catch (error) { + logger.error( + `Error while importing plugin ${absolutePath}: ${error as string}`, + ); + } + } + + logger.info(`Registered ${pluginCount} plugin(s)`); } diff --git a/src/core/plugins/plugin-manager.ts b/src/core/plugins/plugin-manager.ts index 55453d0..83d623f 100644 --- a/src/core/plugins/plugin-manager.ts +++ b/src/core/plugins/plugin-manager.ts @@ -1,120 +1,120 @@ -import { EventEmitter } from "events"; -import { logger } from "../utils/logger"; -import type { Plugin } from "~/typings/plugin"; +import { EventEmitter } from "node:events"; import type { ContainerInfo } from "~/typings/docker"; +import type { Plugin } from "~/typings/plugin"; +import { logger } from "../utils/logger"; class PluginManager extends EventEmitter { - private plugins: Map = new Map(); - - register(plugin: Plugin) { - try { - this.plugins.set(plugin.name, plugin); - logger.debug(`Registered plugin: ${plugin.name}`); - } catch (error) { - logger.error( - `Registering plugin ${plugin.name} failed: ${error as string}`, - ); - } - } - - unregister(name: string) { - this.plugins.delete(name); - } - - getLoadedPlugins(): string[] { - return Array.from(this.plugins.keys()); - } - - // Trigger plugin flows: - handleContainerStop(containerInfo: ContainerInfo) { - this.plugins.forEach((plugin) => { - plugin.onContainerStop?.(containerInfo); - }); - } - - handleContainerStart(containerInfo: ContainerInfo) { - this.plugins.forEach((plugin) => { - plugin.onContainerStart?.(containerInfo); - }); - } - - handleContainerExit(containerInfo: ContainerInfo) { - this.plugins.forEach((plugin) => { - plugin.onContainerExit?.(containerInfo); - }); - } - - handleContainerCreate(containerInfo: ContainerInfo) { - this.plugins.forEach((plugin) => { - plugin.onContainerCreate?.(containerInfo); - }); - } - - handleContainerDestroy(containerInfo: ContainerInfo) { - this.plugins.forEach((plugin) => { - plugin.onContainerDestroy?.(containerInfo); - }); - } - - handleContainerPause(containerInfo: ContainerInfo) { - this.plugins.forEach((plugin) => { - plugin.onContainerPause?.(containerInfo); - }); - } - - handleContainerUnpause(containerInfo: ContainerInfo) { - this.plugins.forEach((plugin) => { - plugin.onContainerUnpause?.(containerInfo); - }); - } - - handleContainerRestart(containerInfo: ContainerInfo) { - this.plugins.forEach((plugin) => { - plugin.onContainerRestart?.(containerInfo); - }); - } - - handleContainerUpdate(containerInfo: ContainerInfo) { - this.plugins.forEach((plugin) => { - plugin.onContainerUpdate?.(containerInfo); - }); - } - - handleContainerRename(containerInfo: ContainerInfo) { - this.plugins.forEach((plugin) => { - plugin.onContainerRename?.(containerInfo); - }); - } - - handleContainerHealthStatus(containerInfo: ContainerInfo) { - this.plugins.forEach((plugin) => { - plugin.onContainerHealthStatus?.(containerInfo); - }); - } - - handleHostUnreachable(host: string, err: string) { - this.plugins.forEach((plugin) => { - plugin.onHostUnreachable?.(host, err); - }); - } - - handleHostReachableAgain(host: string) { - this.plugins.forEach((plugin) => { - plugin.onHostReachableAgain?.(host); - }); - } - - handleContainerKill(containerInfo: ContainerInfo) { - this.plugins.forEach((plugin) => { - plugin.onContainerKill?.(containerInfo); - }); - } - - handleContainerDie(containerInfo: ContainerInfo) { - this.plugins.forEach((plugin) => { - plugin.handleContainerDie?.(containerInfo); - }); - } + private plugins: Map = new Map(); + + register(plugin: Plugin) { + try { + this.plugins.set(plugin.name, plugin); + logger.debug(`Registered plugin: ${plugin.name}`); + } catch (error) { + logger.error( + `Registering plugin ${plugin.name} failed: ${error as string}`, + ); + } + } + + unregister(name: string) { + this.plugins.delete(name); + } + + getLoadedPlugins(): string[] { + return Array.from(this.plugins.keys()); + } + + // Trigger plugin flows: + handleContainerStop(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerStop?.(containerInfo); + } + } + + handleContainerStart(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerStart?.(containerInfo); + } + } + + handleContainerExit(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerExit?.(containerInfo); + } + } + + handleContainerCreate(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerCreate?.(containerInfo); + } + } + + handleContainerDestroy(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerDestroy?.(containerInfo); + } + } + + handleContainerPause(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerPause?.(containerInfo); + } + } + + handleContainerUnpause(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerUnpause?.(containerInfo); + } + } + + handleContainerRestart(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerRestart?.(containerInfo); + } + } + + handleContainerUpdate(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerUpdate?.(containerInfo); + } + } + + handleContainerRename(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerRename?.(containerInfo); + } + } + + handleContainerHealthStatus(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerHealthStatus?.(containerInfo); + } + } + + handleHostUnreachable(host: string, err: string) { + for (const [, plugin] of this.plugins) { + plugin.onHostUnreachable?.(host, err); + } + } + + handleHostReachableAgain(host: string) { + for (const [, plugin] of this.plugins) { + plugin.onHostReachableAgain?.(host); + } + } + + handleContainerKill(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerKill?.(containerInfo); + } + } + + handleContainerDie(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.handleContainerDie?.(containerInfo); + } + } } export const pluginManager = new PluginManager(); diff --git a/src/core/stacks/controller.ts b/src/core/stacks/controller.ts index 19bd082..b6f6ddd 100644 --- a/src/core/stacks/controller.ts +++ b/src/core/stacks/controller.ts @@ -1,330 +1,347 @@ -import { dbFunctions } from "~/core/database"; +import { rm } from "node:fs/promises"; +import DockerCompose from "docker-compose"; import YAML from "yaml"; +import { dbFunctions } from "~/core/database"; import { logger } from "~/core/utils/logger"; -import DockerCompose from "docker-compose"; -import type { Stack, ComposeSpec } from "~/typings/docker-compose"; +import { postToClient } from "~/routes/live-stacks"; import type { stacks_config } from "~/typings/database"; -import { rm } from "node:fs/promises"; +import type { ComposeSpec, Stack } from "~/typings/docker-compose"; import { findObjectByKey } from "../utils/helpers"; -import { postToClient } from "~/routes/live-stacks"; const wrapProgressCallback = (progressCallback?: (log: string) => void) => { - return progressCallback - ? (chunk: Buffer, streamSource?: "stdout" | "stderr") => { - const log = chunk.toString(); - progressCallback(log); - } - : undefined; + return progressCallback + ? (chunk: Buffer, streamSource?: "stdout" | "stderr") => { + const log = chunk.toString(); + progressCallback(log); + } + : undefined; }; async function getStackName(stack_id: number): Promise { - logger.debug(`Fetching stack name for id ${stack_id}`); - const stacks = dbFunctions.getStacks(); - const stack = findObjectByKey(stacks, "id", stack_id); - if (!stack) { - throw new Error(`Stack with id ${stack_id} not found`); - } - return stack.name; + logger.debug(`Fetching stack name for id ${stack_id}`); + const stacks = dbFunctions.getStacks(); + const stack = findObjectByKey(stacks, "id", stack_id); + if (!stack) { + throw new Error(`Stack with id ${stack_id} not found`); + } + return stack.name; } async function runStackCommand( - stack_id: number, - command: ( - cwd: string, - progressCallback?: (log: string) => void - ) => Promise, - action: string + stack_id: number, + command: ( + cwd: string, + progressCallback?: (log: string) => void, + ) => Promise, + action: string, ): Promise { - try { - const stackName = await getStackName(stack_id); - const stackPath = await getStackPath({ - id: stack_id, - name: stackName, - } as Stack); + try { + const stackName = await getStackName(stack_id); + const stackPath = await getStackPath({ + id: stack_id, + name: stackName, + } as Stack); - const progressCallback = (log: string) => { - postToClient({ - type: "stack-progress", - data: { - stack_id, - action, - message: log.trim(), - timestamp: new Date().toISOString(), - }, - }); - }; + const progressCallback = (log: string) => { + postToClient({ + type: "stack-progress", + data: { + stack_id, + action, + message: log.trim(), + timestamp: new Date().toISOString(), + }, + }); + }; - return await command(stackPath, progressCallback); - } catch (error: any) { - postToClient({ - type: "stack-error", - data: { - stack_id, - action, - message: error.message || String(error), - timestamp: new Date().toISOString(), - }, - }); - throw new Error( - `Error while ${action} stack "${stack_id}": ${error.message || error}` - ); - } + return await command(stackPath, progressCallback); + } catch (error) { + postToClient({ + type: "stack-error", + data: { + stack_id, + action, + message: String(error), + timestamp: new Date().toISOString(), + }, + }); + throw new Error( + `Error while ${action} stack "${stack_id}": ${String(error)}`, + ); + } } async function getStackPath(stack: Stack): Promise { - const stackName = stack.name.trim().replace(/\s+/g, "_"); - return `stacks/${stackName}`; + const stackName = stack.name.trim().replace(/\s+/g, "_"); + return `stacks/${stackName}`; } async function createStackYAML(compose_spec: Stack): Promise { - const yaml = YAML.stringify(compose_spec.compose_spec); - const stackPath = await getStackPath(compose_spec); - await Bun.write(`${stackPath}/docker-compose.yaml`, yaml, { - createPath: true, - }); + const yaml = YAML.stringify(compose_spec.compose_spec); + const stackPath = await getStackPath(compose_spec); + await Bun.write(`${stackPath}/docker-compose.yaml`, yaml, { + createPath: true, + }); } export async function deployStack( - stack: ComposeSpec, - name: string, - version: number, - source: string, - automatic_reboot_on_error: boolean, - isCustom: boolean, - image_updates: boolean, - stack_prefix?: string + stack: ComposeSpec, + name: string, + version: number, + source: string, + automatic_reboot_on_error: boolean, + isCustom: boolean, + image_updates: boolean, + stack_prefix?: string, ): Promise { - let stackId: number; + let stackId: number; - try { - logger.debug(`Deploying Stack: ${JSON.stringify(stack)}`); - const serviceCount = stack.services - ? Object.keys(stack.services).length - : 0; - const resolvedPrefix = stack_prefix ?? ""; + try { + logger.debug(`Deploying Stack: ${JSON.stringify(stack)}`); + const serviceCount = stack.services + ? Object.keys(stack.services).length + : 0; + const resolvedPrefix = stack_prefix ?? ""; - const stack_config: stacks_config = { - id: 0, - name, - version, - source, - stack_prefix: resolvedPrefix, - automatic_reboot_on_error, - container_count: serviceCount, - custom: isCustom, - image_updates, - }; + const stack_config: stacks_config = { + id: 0, + name, + version, + source, + stack_prefix: resolvedPrefix, + automatic_reboot_on_error, + container_count: serviceCount, + custom: isCustom, + image_updates, + }; - if (!name) { - throw new Error("Stack name needed"); - } + if (!name) { + throw new Error("Stack name needed"); + } - stackId = dbFunctions.addStack(stack_config) as number; - postToClient({ - type: "stack-status", - data: { - stack_id: stackId, - status: "pending", - message: "Creating stack configuration", - }, - }); + stackId = dbFunctions.addStack(stack_config) as number; + postToClient({ + type: "stack-status", + data: { + stack_id: stackId, + status: "pending", + message: "Creating stack configuration", + }, + }); - const stackYaml: Stack = { - id: stackId, - name, - source, - version, - compose_spec: stack, - }; + const stackYaml: Stack = { + id: stackId, + name, + source, + version, + compose_spec: stack, + }; - await createStackYAML(stackYaml); + await createStackYAML(stackYaml); - await runStackCommand( - stackId, - (cwd, progressCallback) => - DockerCompose.upAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "deploying" - ); + await runStackCommand( + stackId, + (cwd, progressCallback) => + DockerCompose.upAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "deploying", + ); - postToClient({ - type: "stack-status", - data: { - stack_id: stackId, - status: "deployed", - message: "Stack deployed successfully", - }, - }); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - postToClient({ - type: "stack-error", - data: { - stack_id: 0, - action: "deploying", - message: errorMsg, - timestamp: new Date().toISOString(), - }, - }); - throw new Error(errorMsg); - } + postToClient({ + type: "stack-status", + data: { + stack_id: stackId, + status: "deployed", + message: "Stack deployed successfully", + }, + }); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + postToClient({ + type: "stack-error", + data: { + stack_id: 0, + action: "deploying", + message: errorMsg, + timestamp: new Date().toISOString(), + }, + }); + throw new Error(errorMsg); + } } export async function stopStack(stack_id: number): Promise { - // Note the await to discard the result (convert to void) - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.downAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "stopping" - ); + // Note the await to discard the result (convert to void) + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.downAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "stopping", + ); } export async function startStack(stack_id: number): Promise { - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.upAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "starting" - ); + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.upAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "starting", + ); } export async function pullStackImages(stack_id: number): Promise { - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.pullAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "pulling-images" - ); + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.pullAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "pulling-images", + ); } export async function restartStack(stack_id: number): Promise { - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.restartAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "restarting" - ); + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.restartAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "restarting", + ); } export async function getStackStatus( - stack_id: number + stack_id: number, + //biome-ignore lint/suspicious/noExplicitAny: ): Promise> { - // Wrap the returned status value to match Promise if that is the expectation. - // In this case, if you need the status, you might adjust the type signature. - const status = await runStackCommand( - stack_id, - async (cwd) => { - const rawStatus = await DockerCompose.ps({ cwd }); - return rawStatus.data.services.reduce((acc: any, service: any) => { - acc[service.name] = service.state; - return acc; - }, {}); - }, - "status-check" - ); - return status; + const status = await runStackCommand( + stack_id, + async (cwd) => { + const rawStatus = await DockerCompose.ps({ cwd }); + //biome-ignore lint/suspicious/noExplicitAny: + return rawStatus.data.services.reduce((acc: any, service: any) => { + acc[service.name] = service.state; + return acc; + }, {}); + }, + "status-check", + ); + return status; } export async function removeStack(stack_id: number): Promise { - try { - await runStackCommand( - stack_id, - async (cwd, progressCallback) => { - await DockerCompose.down({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }); - }, - "removing" - ); + try { + await runStackCommand( + stack_id, + async (cwd, progressCallback) => { + await DockerCompose.down({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }); + }, + "removing", + ); - const stackName = await getStackName(stack_id); - const stackPath = await getStackPath({ - id: stack_id, - name: stackName, - } as Stack); + const stackName = await getStackName(stack_id); + const stackPath = await getStackPath({ + id: stack_id, + name: stackName, + } as Stack); - try { - await rm(stackPath, { recursive: true }); - } catch (error: any) { - if (error.code !== "ENOENT") throw error; - } + try { + await rm(stackPath, { recursive: true }); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + postToClient({ + type: "stack-error", + data: { + stack_id, + action: "removing", + message: errorMsg, + timestamp: new Date().toISOString(), + }, + }); + throw new Error(errorMsg); + } - dbFunctions.deleteStack(stack_id); - postToClient({ - type: "stack-removed", - data: { - stack_id, - message: "Stack removed successfully", - }, - }); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - postToClient({ - type: "stack-error", - data: { - stack_id, - action: "removing", - message: errorMsg, - timestamp: new Date().toISOString(), - }, - }); - throw new Error(errorMsg); - } + dbFunctions.deleteStack(stack_id); + postToClient({ + type: "stack-removed", + data: { + stack_id, + message: "Stack removed successfully", + }, + }); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + postToClient({ + type: "stack-error", + data: { + stack_id, + action: "removing", + message: errorMsg, + timestamp: new Date().toISOString(), + }, + }); + throw new Error(errorMsg); + } } +//biome-ignore lint/suspicious/noExplicitAny: export async function getAllStacksStatus(): Promise> { - try { - const stacks = dbFunctions.getStacks(); + try { + const stacks = dbFunctions.getStacks(); - const statusResults = await Promise.all( - stacks.map(async (stack) => { - const status = await runStackCommand( - stack.id as number, - async (cwd) => { - const rawStatus = await DockerCompose.ps({ cwd }); - return rawStatus.data.services.reduce((acc: any, service: any) => { - acc[service.name] = service.state; - return acc; - }, {}); - }, - "status-check" - ); - return { stackId: stack.id, status }; - }) - ); + const statusResults = await Promise.all( + stacks.map(async (stack) => { + const status = await runStackCommand( + stack.id as number, + async (cwd) => { + const rawStatus = await DockerCompose.ps({ cwd }); + //biome-ignore lint/suspicious/noExplicitAny: + return rawStatus.data.services.reduce((acc: any, service: any) => { + acc[service.name] = service.state; + return acc; + }, {}); + }, + "status-check", + ); + return { stackId: stack.id, status }; + }), + ); - return statusResults.reduce((acc, { stackId, status }) => { - // Ensure stackId is used as a string if necessary, e.g. - acc[String(stackId)] = status; - return acc; - }, {} as Record); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - throw new Error(errorMsg); - } + return statusResults.reduce( + (acc, { stackId, status }) => { + // Ensure stackId is used as a string if necessary, e.g. + acc[String(stackId)] = status; + return acc; + }, + //biome-ignore lint/suspicious/noExplicitAny: + {} as Record, + ); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + throw new Error(errorMsg); + } } diff --git a/src/core/utils/calculations.ts b/src/core/utils/calculations.ts index 60ab40b..1b5c893 100644 --- a/src/core/utils/calculations.ts +++ b/src/core/utils/calculations.ts @@ -1,45 +1,37 @@ import type Docker from "dockerode"; const calculateCpuPercent = (stats: Docker.ContainerStats): number => { - if (stats == null) { - return 0.0; - } + const cpuDelta = + stats.cpu_stats.cpu_usage.total_usage - + stats.precpu_stats.cpu_usage.total_usage; + const systemDelta = + stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage; - const cpuDelta = - stats.cpu_stats.cpu_usage.total_usage - - stats.precpu_stats.cpu_usage.total_usage; - const systemDelta = - stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage; + if (cpuDelta <= 0) { + return 0.0000001; + } - if (cpuDelta <= 0) { - return 0; - } + if (systemDelta <= 0) { + return 0.0000001; + } - if (systemDelta <= 0) { - return 0; - } + const data = (cpuDelta / systemDelta) * 100; - const data = (cpuDelta / systemDelta) * 100; + if (data === null) { + return 0.0000001; + } - if (data === null) { - return 0; - } - - return data; + return data; }; const calculateMemoryUsage = (stats: Docker.ContainerStats): number => { - if (stats == null) { - return 0; - } - - const data = (stats.memory_stats.usage / stats.memory_stats.limit) * 100; + if (stats.memory_stats.usage === null) { + return 0.0000001; + } - if (data === null) { - return 0; - } + const data = (stats.memory_stats.usage / stats.memory_stats.limit) * 100; - return data; + return data; }; export { calculateCpuPercent, calculateMemoryUsage }; diff --git a/src/core/utils/change-me-checker.ts b/src/core/utils/change-me-checker.ts index 7486048..d5aefb4 100644 --- a/src/core/utils/change-me-checker.ts +++ b/src/core/utils/change-me-checker.ts @@ -1,18 +1,18 @@ -import { readFile } from "fs/promises"; +import { readFile } from "node:fs/promises"; import { logger } from "~/core/utils/logger"; export async function checkFileForChangeMe(filePath: string) { - const regex = /change[\W_]*me/i; - let content = ""; - try { - content = await readFile(filePath, "utf-8"); - } catch (error) { - logger.error("Error reading file:", error); - } + const regex = /change[\W_]*me/i; + let content = ""; + try { + content = await readFile(filePath, "utf-8"); + } catch (error) { + logger.error("Error reading file:", error); + } - if (regex.test(content)) { - throw new Error( - `The file contains ${regex.exec(content)}. Please update it.` - ); - } + if (regex.test(content)) { + throw new Error( + `The file contains ${regex.exec(content)}. Please update it.`, + ); + } } diff --git a/src/core/utils/helpers.ts b/src/core/utils/helpers.ts index 689e566..ab13dd4 100644 --- a/src/core/utils/helpers.ts +++ b/src/core/utils/helpers.ts @@ -1,11 +1,11 @@ import { logger } from "./logger"; export function findObjectByKey( - array: T[], - key: keyof T, - value: T[keyof T] + array: T[], + key: keyof T, + value: T[keyof T], ): T | undefined { - logger.debug(`Searching ${String(key)}`); - const data = array.find((item) => item[key] === value); - return data; + logger.debug(`Searching ${String(key)}`); + const data = array.find((item) => item[key] === value); + return data; } diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts index a10dfd2..5320876 100644 --- a/src/core/utils/logger.ts +++ b/src/core/utils/logger.ts @@ -1,174 +1,180 @@ -import path from "path"; -import wrapAnsi from "wrap-ansi"; -import chalk, { ChalkInstance } from "chalk"; +import path from "node:path"; +import chalk, { type ChalkInstance } from "chalk"; import type { TransformableInfo } from "logform"; import { createLogger, format, transports } from "winston"; +import wrapAnsi from "wrap-ansi"; import { dbFunctions } from "~/core/database"; import { logToClients } from "~/routes/live-logs"; -import { log_message } from "~/typings/database"; +import type { log_message } from "~/typings/database"; + +import { backupInProgress } from "../database/_dbState"; const padNewlines = process.env.PAD_NEW_LINES !== "false"; type LogLevel = - | "error" - | "warn" - | "info" - | "debug" - | "verbose" - | "silly" - | "task" - | "ut"; - + | "error" + | "warn" + | "info" + | "debug" + | "verbose" + | "silly" + | "task" + | "ut"; + +// biome-ignore lint/suspicious/noControlCharactersInRegex: const ansiRegex = /\x1B\[[0-?9;]*[mG]/g; const formatTerminalMessage = (message: string, prefix: string): string => { - try { - const cleanPrefix = prefix.replace(ansiRegex, ""); - const maxWidth = process.stdout.columns || 80; - const wrapWidth = Math.max(maxWidth - cleanPrefix.length - 3, 20); - - if (!padNewlines) return message; - - const wrapped = wrapAnsi(message, wrapWidth, { - trim: true, - hard: true, - wordWrap: true, - }); - - return wrapped - .split("\n") - .map((line, index) => { - return index === 0 ? line : `${" ".repeat(cleanPrefix.length)}${line}`; - }) - .join("\n"); - } catch (error) { - console.error("Error formatting terminal message:", error); - return message; - } + try { + const cleanPrefix = prefix.replace(ansiRegex, ""); + const maxWidth = process.stdout.columns || 80; + const wrapWidth = Math.max(maxWidth - cleanPrefix.length - 3, 20); + + if (!padNewlines) return message; + + const wrapped = wrapAnsi(message, wrapWidth, { + trim: true, + hard: true, + wordWrap: true, + }); + + return wrapped + .split("\n") + .map((line, index) => { + return index === 0 ? line : `${" ".repeat(cleanPrefix.length)}${line}`; + }) + .join("\n"); + } catch (error) { + console.error("Error formatting terminal message:", error); + return message; + } }; const levelColors: Record = { - error: chalk.red.bold, - warn: chalk.yellow.bold, - info: chalk.green.bold, - debug: chalk.blue.bold, - verbose: chalk.cyan.bold, - silly: chalk.magenta.bold, - task: chalk.cyan.bold, - ut: chalk.hex("#9D00FF"), + error: chalk.red.bold, + warn: chalk.yellow.bold, + info: chalk.green.bold, + debug: chalk.blue.bold, + verbose: chalk.cyan.bold, + silly: chalk.magenta.bold, + task: chalk.cyan.bold, + ut: chalk.hex("#9D00FF"), }; const handleWebSocketLog = (log: log_message) => { - try { - logToClients(log); - } catch (error) { - console.error( - `WebSocket logging failed: ${ - error instanceof Error ? error.message : error - }` - ); - } + try { + logToClients(log); + } catch (error) { + console.error( + `WebSocket logging failed: ${ + error instanceof Error ? error.message : error + }`, + ); + } }; const handleDatabaseLog = (log: log_message): void => { - try { - dbFunctions.addLogEntry(log); - } catch (error) { - console.error( - `Database logging failed: ${ - error instanceof Error ? error.message : error - }` - ); - } + if (backupInProgress) { + return; + } + try { + dbFunctions.addLogEntry(log); + } catch (error) { + console.error( + `Database logging failed: ${ + error instanceof Error ? error.message : error + }`, + ); + } }; export const logger = createLogger({ - level: process.env.LOG_LEVEL || "debug", - format: format.combine( - format.timestamp({ format: "DD/MM HH:mm:ss" }), - format((info) => { - const stack = new Error().stack?.split("\n"); - let file = "unknown"; - let line = 0; - - if (stack) { - for (let i = 2; i < stack.length; i++) { - const lineStr = stack[i].trim(); - if ( - !lineStr.includes("node_modules") && - !lineStr.includes(path.basename(__filename)) - ) { - const matches = lineStr.match(/\(?(.+):(\d+):(\d+)\)?$/); - if (matches) { - file = path.basename(matches[1]); - line = parseInt(matches[2], 10); - break; - } - } - } - } - return { ...info, file, line }; - })(), - format.printf((info) => { - const { timestamp, level, message, file, line } = - info as TransformableInfo & log_message; - let processedLevel = level as LogLevel; - let processedMessage = String(message); - - if (processedMessage.startsWith("__task__")) { - processedMessage = processedMessage - .replace(/__task__/g, "") - .trimStart(); - processedLevel = "task"; - if (processedMessage.startsWith("__db__")) { - processedMessage = processedMessage - .replace(/__db__/g, "") - .trimStart(); - processedMessage = `${chalk.magenta("DB")} ${processedMessage}`; - } - } else if (processedMessage.startsWith("__UT__")) { - processedMessage = processedMessage.replace(/__UT__/g, "").trimStart(); - processedLevel = "ut"; - } - - if (file.endsWith("plugin.ts")) { - processedMessage = `[ ${chalk.grey(file)} ] ${processedMessage}`; - } - - const paddedLevel = processedLevel.toUpperCase().padEnd(5); - const coloredLevel = (levelColors[processedLevel] || chalk.white)( - paddedLevel - ); - const coloredContext = chalk.cyan(`${file}:${line}`); - const coloredTimestamp = chalk.yellow(timestamp); - - const prefix = `${paddedLevel} [ ${timestamp} ] - `; - const combinedContent = `${processedMessage} - ${coloredContext}`; - - const formattedMessage = padNewlines - ? formatTerminalMessage(combinedContent, prefix) - : combinedContent; - - handleDatabaseLog({ - level: processedLevel, - timestamp, - message: processedMessage, - file, - line, - }); - handleWebSocketLog({ - level: processedLevel, - timestamp, - message: processedMessage, - file, - line, - }); - - return `${coloredLevel} [ ${coloredTimestamp} ] - ${formattedMessage}`; - }) - ), - transports: [new transports.Console()], + level: process.env.LOG_LEVEL || "debug", + format: format.combine( + format.timestamp({ format: "DD/MM HH:mm:ss" }), + format((info) => { + const stack = new Error().stack?.split("\n"); + let file = "unknown"; + let line = 0; + + if (stack) { + for (let i = 2; i < stack.length; i++) { + const lineStr = stack[i].trim(); + if ( + !lineStr.includes("node_modules") && + !lineStr.includes(path.basename(__filename)) + ) { + const matches = lineStr.match(/\(?(.+):(\d+):(\d+)\)?$/); + if (matches) { + file = path.basename(matches[1]); + line = Number.parseInt(matches[2], 10); + break; + } + } + } + } + return { ...info, file, line }; + })(), + format.printf((info) => { + const { timestamp, level, message, file, line } = + info as TransformableInfo & log_message; + let processedLevel = level as LogLevel; + let processedMessage = String(message); + + if (processedMessage.startsWith("__task__")) { + processedMessage = processedMessage + .replace(/__task__/g, "") + .trimStart(); + processedLevel = "task"; + if (processedMessage.startsWith("__db__")) { + processedMessage = processedMessage + .replace(/__db__/g, "") + .trimStart(); + processedMessage = `${chalk.magenta("DB")} ${processedMessage}`; + } + } else if (processedMessage.startsWith("__UT__")) { + processedMessage = processedMessage.replace(/__UT__/g, "").trimStart(); + processedLevel = "ut"; + } + + if (file.endsWith("plugin.ts")) { + processedMessage = `[ ${chalk.grey(file)} ] ${processedMessage}`; + } + + const paddedLevel = processedLevel.toUpperCase().padEnd(5); + const coloredLevel = (levelColors[processedLevel] || chalk.white)( + paddedLevel, + ); + const coloredContext = chalk.cyan(`${file}:${line}`); + const coloredTimestamp = chalk.yellow(timestamp); + + const prefix = `${paddedLevel} [ ${timestamp} ] - `; + const combinedContent = `${processedMessage} - ${coloredContext}`; + + const formattedMessage = padNewlines + ? formatTerminalMessage(combinedContent, prefix) + : combinedContent; + + handleDatabaseLog({ + level: processedLevel, + timestamp, + message: processedMessage, + file, + line, + }); + handleWebSocketLog({ + level: processedLevel, + timestamp, + message: processedMessage, + file, + line, + }); + + return `${coloredLevel} [ ${coloredTimestamp} ] - ${formattedMessage}`; + }), + ), + transports: [new transports.Console()], }); diff --git a/src/core/utils/package-json.ts b/src/core/utils/package-json.ts index 9147d2f..20958a4 100644 --- a/src/core/utils/package-json.ts +++ b/src/core/utils/package-json.ts @@ -1,25 +1,25 @@ import packageJson from "~/../package.json"; const { version, description, license, dependencies, devDependencies } = - packageJson; + packageJson; let { contributors } = packageJson; const authorName = packageJson.author.name; const authorEmail = packageJson.author.email; const authorWebsite = packageJson.author.url; -if ((contributors = [])) { - contributors = [":(" as never]; +if (contributors.length === 0) { + contributors = [":(" as never]; } export { - version, - description, - authorName, - authorEmail, - authorWebsite, - license, - contributors, - dependencies, - devDependencies, + version, + description, + authorName, + authorEmail, + authorWebsite, + license, + contributors, + dependencies, + devDependencies, }; diff --git a/src/core/utils/response-handler.ts b/src/core/utils/response-handler.ts index 60a11ea..8bfe6ec 100644 --- a/src/core/utils/response-handler.ts +++ b/src/core/utils/response-handler.ts @@ -3,36 +3,41 @@ import { logger } from "~/core/utils/logger"; import type { set } from "~/typings/elysiajs"; export const responseHandler = { - error( - set: set, - error: string, - response_message: string, - error_code?: number - ) { - set.status = error_code || 500; - logger.error(`${response_message} - ${error}`); - return { error: `${response_message}` }; - }, + error( + set: set, + error: string, + response_message: string, + error_code?: number, + ) { + set.status = error_code || 500; + logger.error(`${response_message} - ${error}`); + return { error: `${response_message}` }; + }, - ok(set: set, response_message: string) { - set.status = 200; - logger.debug(response_message); - return { success: true }; - }, + ok(set: set, response_message: string) { + set.status = 200; + logger.debug(response_message); + return { success: true, message: response_message }; + }, - simple_error(set: set, response_message: string, status_code?: number) { - set.status = status_code || 502; - logger.warn(response_message); - return { error: response_message }; - }, + simple_error(set: set, response_message: string, status_code?: number) { + set.status = status_code || 502; + logger.warn(response_message); + return { error: response_message }; + }, - reject(set: set, reject: any, response_message: string, error?: string) { - set.status = 501; - if (error) { - logger.error(`${response_message} - ${error}`); - } else { - logger.error(response_message); - } - return reject(new Error(response_message)); - }, + reject( + set: set, + reject: CallableFunction, + response_message: string, + error?: string, + ) { + set.status = 501; + if (error) { + logger.error(`${response_message} - ${error}`); + } else { + logger.error(response_message); + } + return reject(new Error(response_message)); + }, }; diff --git a/src/index.ts b/src/index.ts index cf5edef..8090d9a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,168 +1,168 @@ -import { Elysia } from "elysia"; +import { serverTiming } from "@elysiajs/server-timing"; import staticPlugin from "@elysiajs/static"; import { swagger } from "@elysiajs/swagger"; -import { serverTiming } from "@elysiajs/server-timing"; +import { Elysia } from "elysia"; -import { logger } from "~/core/utils/logger"; import { dbFunctions } from "~/core/database"; -import { loadPlugins } from "~/core/plugins/loader"; -import { setSchedules } from "~/core/docker/scheduler"; import { monitorDockerEvents } from "~/core/docker/monitor"; -import { swaggerReadme } from "~/core/utils/swagger-readme"; +import { setSchedules } from "~/core/docker/scheduler"; +import { loadPlugins } from "~/core/plugins/loader"; +import { logger } from "~/core/utils/logger"; import { - authorWebsite, - contributors, - license, + authorWebsite, + contributors, + license, } from "~/core/utils/package-json"; +import { swaggerReadme } from "~/core/utils/swagger-readme"; import { validateApiKey } from "~/middleware/auth"; -import { backendLogs } from "~/routes/logs"; -import { utilRoutes } from "~/routes/utils"; -import { liveLogs } from "~/routes/live-logs"; -import { stackRoutes } from "~/routes/stacks"; import { apiConfigRoutes } from "~/routes/api-config"; import { dockerRoutes } from "~/routes/docker-manager"; import { dockerStatsRoutes } from "~/routes/docker-stats"; import { dockerWebsocketRoutes } from "~/routes/docker-websocket"; +import { liveLogs } from "~/routes/live-logs"; +import { backendLogs } from "~/routes/logs"; +import { stackRoutes } from "~/routes/stacks"; +import { utilRoutes } from "~/routes/utils"; import { liveStacks } from "./routes/live-stacks"; -import { config } from "~/typings/database"; +import type { config } from "~/typings/database"; console.log(""); logger.info("Starting DockStatAPI"); export const DockStatAPI = new Elysia() - .use(staticPlugin()) - .use(serverTiming()) - .use( - swagger({ - documentation: { - info: { - title: "DockStatAPI", - version: "3.0.0", - description: swaggerReadme, - }, - components: { - securitySchemes: { - apiKeyAuth: { - type: "apiKey", - name: "x-api-key", - in: "header", - description: "API key for authentication", - }, - }, - }, - security: [ - { - apiKeyAuth: [], - }, - ], - tags: [ - { - name: "Statistics", - description: - "All endpoints for fetching statistics of hosts / containers", - }, - { - name: "Management", - description: "Various endpoints for managing DockStatAPI", - }, - { - name: "Stacks", - description: "DockStat's Stack functionality", - }, - { - name: "Utils", - description: "Various utilities which might be useful", - }, - ], - }, - }) - ) - .onBeforeHandle(async (context) => { - const { path, request, set } = context; - - if (path === "/health" || path.startsWith("/swagger")) { - logger.info(`Requested unguarded route: ${path}`); - return; - } - - const validation = await validateApiKey(request, set); - - if (validation.error) { - set.status = 400; - set.headers["Content-Type"] = "application/json"; - return { error: validation.error }; - } - }) - .use(dockerRoutes) - .use(dockerStatsRoutes) - .use(backendLogs) - .use(dockerWebsocketRoutes) - .use(apiConfigRoutes) - .use(utilRoutes) - .use(stackRoutes) - .use(utilRoutes) - .use(liveLogs) - .use(liveStacks) - .get("/health", () => ({ status: "healthy" }), { tags: ["Utils"] }) - .onError(({ code, set, path }) => { - if (code === "NOT_FOUND") { - logger.warn(`Unknown route (${path}), showing error page!`); - set.status = 404; - set.headers["Content-Type"] = "text/html"; - return Bun.file("public/404.html"); - } - }); + .use(staticPlugin()) + .use(serverTiming()) + .use( + swagger({ + documentation: { + info: { + title: "DockStatAPI", + version: "3.0.0", + description: swaggerReadme, + }, + components: { + securitySchemes: { + apiKeyAuth: { + type: "apiKey", + name: "x-api-key", + in: "header", + description: "API key for authentication", + }, + }, + }, + security: [ + { + apiKeyAuth: [], + }, + ], + tags: [ + { + name: "Statistics", + description: + "All endpoints for fetching statistics of hosts / containers", + }, + { + name: "Management", + description: "Various endpoints for managing DockStatAPI", + }, + { + name: "Stacks", + description: "DockStat's Stack functionality", + }, + { + name: "Utils", + description: "Various utilities which might be useful", + }, + ], + }, + }), + ) + .onBeforeHandle(async (context) => { + const { path, request, set } = context; + + if (path === "/health" || path.startsWith("/swagger")) { + logger.info(`Requested unguarded route: ${path}`); + return; + } + + const validation = await validateApiKey(request, set); + + if (validation.error) { + set.status = 400; + set.headers["Content-Type"] = "application/json"; + return { error: validation.error }; + } + }) + .use(dockerRoutes) + .use(dockerStatsRoutes) + .use(backendLogs) + .use(dockerWebsocketRoutes) + .use(apiConfigRoutes) + .use(utilRoutes) + .use(stackRoutes) + .use(utilRoutes) + .use(liveLogs) + .use(liveStacks) + .get("/health", () => ({ status: "healthy" }), { tags: ["Utils"] }) + .onError(({ code, set, path }) => { + if (code === "NOT_FOUND") { + logger.warn(`Unknown route (${path}), showing error page!`); + set.status = 404; + set.headers["Content-Type"] = "text/html"; + return Bun.file("public/404.html"); + } + }); async function startServer() { - try { - try { - await loadPlugins("./src/plugins"); - } catch (error) { - throw new Error(`Failed to load plugins: ${error}`); - } - - try { - await setSchedules(); - } catch (error) { - throw new Error(`Failed to set schedules: ${error}`); - } - - monitorDockerEvents().catch((error) => { - logger.error(`Monitoring Error: ${error}`); - }); - - const configData = dbFunctions.getConfig() as config[]; - const apiKey = configData[0].api_key; - - if (apiKey === "changeme") { - logger.warn( - "Default API Key of 'changeme' detected. Please change your API Key via the `/config/update` route!" - ); - } - - try { - DockStatAPI.listen(3000, ({ hostname, port }) => { - console.log("----- [ ############## ]"); - logger.info(`DockStatAPI is running at http://${hostname}:${port}`); - logger.info( - `Swagger API Documentation available at http://${hostname}:${port}/swagger` - ); - logger.info(`License: ${license}`); - logger.info(`Author: ${authorWebsite}`); - logger.info(`Contributors: ${contributors}`); - }); - } catch (error) { - logger.error("Failed to start server:", error); - process.exit(1); - } - } catch (error) { - logger.error("Error while starting server:", error); - process.exit(1); - } + try { + try { + await loadPlugins("./src/plugins"); + } catch (error) { + throw new Error(`Failed to load plugins: ${error}`); + } + + try { + await setSchedules(); + } catch (error) { + throw new Error(`Failed to set schedules: ${error}`); + } + + monitorDockerEvents().catch((error) => { + logger.error(`Monitoring Error: ${error}`); + }); + + const configData = dbFunctions.getConfig() as config[]; + const apiKey = configData[0].api_key; + + if (apiKey === "changeme") { + logger.warn( + "Default API Key of 'changeme' detected. Please change your API Key via the `/config/update` route!", + ); + } + + try { + DockStatAPI.listen(3000, ({ hostname, port }) => { + console.log("----- [ ############## ]"); + logger.info(`DockStatAPI is running at http://${hostname}:${port}`); + logger.info( + `Swagger API Documentation available at http://${hostname}:${port}/swagger`, + ); + logger.info(`License: ${license}`); + logger.info(`Author: ${authorWebsite}`); + logger.info(`Contributors: ${contributors}`); + }); + } catch (error) { + logger.error("Failed to start server:", error); + process.exit(1); + } + } catch (error) { + logger.error("Error while starting server:", error); + process.exit(1); + } } await startServer(); diff --git a/src/middleware/auth.ts b/src/middleware/auth.ts index 48aad39..0007793 100644 --- a/src/middleware/auth.ts +++ b/src/middleware/auth.ts @@ -1,82 +1,84 @@ -import { logger } from "~/core/utils/logger"; import { dbFunctions } from "~/core/database"; +import { logger } from "~/core/utils/logger"; -import { set } from "~/typings/elysiajs"; -import { config } from "~/typings/database"; +import type { config } from "~/typings/database"; +import type { set } from "~/typings/elysiajs"; export async function hashApiKey(apiKey: string): Promise { - logger.debug("Hashing API key"); - try { - logger.debug("API key hashed successfully"); - return await Bun.password.hash(apiKey); - } catch (error) { - logger.error("Error hashing API key", error); - throw new Error("Failed to hash API key"); - } + logger.debug("Hashing API key"); + try { + logger.debug("API key hashed successfully"); + return await Bun.password.hash(apiKey); + } catch (error) { + logger.error("Error hashing API key", error); + throw new Error("Failed to hash API key"); + } } async function validateApiKeyHash( - providedKey: string, - storedHash: string + providedKey: string, + storedHash: string, ): Promise { - logger.debug("Validating API key hash"); - try { - const isValid = await Bun.password.verify(providedKey, storedHash); - logger.debug(`API key validation result: ${isValid}`); - return isValid; - } catch (error) { - logger.error("Error validating API key hash", error); - return false; - } + logger.debug("Validating API key hash"); + try { + const isValid = await Bun.password.verify(providedKey, storedHash); + logger.debug(`API key validation result: ${isValid}`); + return isValid; + } catch (error) { + logger.error("Error validating API key hash", error); + return false; + } } async function getApiKeyFromDb( - apiKey: string + apiKey: string, ): Promise<{ hash: string } | null> { - const dbApiKey = (dbFunctions.getConfig() as config[])[0].api_key; - logger.debug(`Querying database for API key: ${apiKey}`); - return Promise.resolve({ - hash: dbApiKey, - }); + const dbApiKey = (dbFunctions.getConfig() as config[])[0].api_key; + logger.debug(`Querying database for API key: ${apiKey}`); + return Promise.resolve({ + hash: dbApiKey, + }); } export async function validateApiKey(request: Request, set: set) { - const apiKey = request.headers.get("x-api-key"); + const apiKey = request.headers.get("x-api-key"); + + if (process.env.NODE_ENV !== "production") { + logger.warn( + "API Key validation deactivated, since running in development mode", + ); + return { apiKey }; + } - if (process.env.NODE_ENV != "production") { - logger.warn( - "API Key validation deactivated, since running in development mode" - ); - return { apiKey }; - } else if (!apiKey) { - logger.error(`API key missing from request ${request.url}`); - set.status = 401; - return { error: "API key required" }; - } + if (!apiKey) { + logger.error(`API key missing from request ${request.url}`); + set.status = 401; + return { error: "API key required" }; + } - logger.debug(`API key validation initiated`); + logger.debug("API key validation initiated"); - try { - const dbRecord = await getApiKeyFromDb(apiKey); + try { + const dbRecord = await getApiKeyFromDb(apiKey); - if (!dbRecord) { - logger.error("API key not found in database"); - set.status = 401; - return { error: "Invalid API key" }; - } + if (!dbRecord) { + logger.error("API key not found in database"); + set.status = 401; + return { error: "Invalid API key" }; + } - const isValid = await validateApiKeyHash(apiKey, dbRecord.hash); + const isValid = await validateApiKeyHash(apiKey, dbRecord.hash); - if (!isValid) { - logger.error("Invalid API key provided"); - set.status = 401; - return { error: "Invalid API key" }; - } + if (!isValid) { + logger.error("Invalid API key provided"); + set.status = 401; + return { error: "Invalid API key" }; + } - return logger.info(`Valid API key used`); - } catch (error) { - logger.error("Error during API key validation", error); - set.status = 500; - return { error: "Internal server error" }; - } + return logger.info("Valid API key used"); + } catch (error) { + logger.error("Error during API key validation", error); + set.status = 500; + return { error: "Internal server error" }; + } } diff --git a/src/plugins/example.plugin.ts b/src/plugins/example.plugin.ts index e9a9775..633eea4 100644 --- a/src/plugins/example.plugin.ts +++ b/src/plugins/example.plugin.ts @@ -1,98 +1,98 @@ import { logger } from "~/core/utils/logger"; -import type { Plugin } from "~/typings/plugin"; import type { ContainerInfo } from "~/typings/docker"; +import type { Plugin } from "~/typings/plugin"; // See https://outline.itsnik.de/s/dockstat/doc/plugin-development-3UBj9gNMKF for more info const ExamplePlugin: Plugin = { - name: "Example Plugin", - - async onContainerStart(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} started on ${containerInfo.hostId}` - ); - }, - - async onContainerStop(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} stopped on ${containerInfo.hostId}` - ); - }, - - async onContainerExit(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} exited on ${containerInfo.hostId}` - ); - }, - - async onContainerCreate(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} created on ${containerInfo.hostId}` - ); - }, - - async onContainerDestroy(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} destroyed on ${containerInfo.hostId}` - ); - }, - - async onContainerPause(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} pause on ${containerInfo.hostId}` - ); - }, - - async onContainerUnpause(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} resumed on ${containerInfo.hostId}` - ); - }, - - async onContainerRestart(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} restarted on ${containerInfo.hostId}` - ); - }, - - async onContainerUpdate(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} updated on ${containerInfo.hostId}` - ); - }, - - async onContainerRename(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} renamed on ${containerInfo.hostId}` - ); - }, - - async onContainerHealthStatus(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} changed status to ${containerInfo.status}` - ); - }, - - async onHostUnreachable(host: string, err: string) { - logger.info(`Server ${host} unreachable - ${err}`); - }, - - async onHostReachableAgain(host: string) { - logger.info(`Server ${host} reachable`); - }, - - async handleContainerDie(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} died on ${containerInfo.hostId}` - ); - }, - - async onContainerKill(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} killed on ${containerInfo.hostId}` - ); - }, + name: "Example Plugin", + + async onContainerStart(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} started on ${containerInfo.hostId}`, + ); + }, + + async onContainerStop(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} stopped on ${containerInfo.hostId}`, + ); + }, + + async onContainerExit(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} exited on ${containerInfo.hostId}`, + ); + }, + + async onContainerCreate(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} created on ${containerInfo.hostId}`, + ); + }, + + async onContainerDestroy(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} destroyed on ${containerInfo.hostId}`, + ); + }, + + async onContainerPause(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} pause on ${containerInfo.hostId}`, + ); + }, + + async onContainerUnpause(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} resumed on ${containerInfo.hostId}`, + ); + }, + + async onContainerRestart(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} restarted on ${containerInfo.hostId}`, + ); + }, + + async onContainerUpdate(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} updated on ${containerInfo.hostId}`, + ); + }, + + async onContainerRename(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} renamed on ${containerInfo.hostId}`, + ); + }, + + async onContainerHealthStatus(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} changed status to ${containerInfo.status}`, + ); + }, + + async onHostUnreachable(host: string, err: string) { + logger.info(`Server ${host} unreachable - ${err}`); + }, + + async onHostReachableAgain(host: string) { + logger.info(`Server ${host} reachable`); + }, + + async handleContainerDie(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} died on ${containerInfo.hostId}`, + ); + }, + + async onContainerKill(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} killed on ${containerInfo.hostId}`, + ); + }, } satisfies Plugin; export default ExamplePlugin; diff --git a/src/plugins/telegram.plugin.ts b/src/plugins/telegram.plugin.ts index eaec24e..0b83d43 100644 --- a/src/plugins/telegram.plugin.ts +++ b/src/plugins/telegram.plugin.ts @@ -1,35 +1,35 @@ import { logger } from "~/core/utils/logger"; -import type { Plugin } from "~/typings/plugin"; import type { ContainerInfo } from "~/typings/docker"; +import type { Plugin } from "~/typings/plugin"; const TELEGRAM_BOT_TOKEN = "CHANGE_ME"; // Replace with your bot token const TELEGRAM_CHAT_ID = "CHANGE_ME"; // Replace with your chat ID const TelegramNotificationPlugin: Plugin = { - name: "Telegram Notification Plugin", - async onContainerStart(containerInfo: ContainerInfo) { - const message = `Container Started: ${containerInfo.name} on ${containerInfo.hostId}`; - try { - const response = await fetch( - `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - chat_id: TELEGRAM_CHAT_ID, - text: message, - }), - } - ); - if (!response.ok) { - logger.error(`HTTP error ${response.status}`); - } - logger.info("Telegram notification sent."); - } catch (error) { - logger.error("Failed to send Telegram notification", error as string); - } - }, + name: "Telegram Notification Plugin", + async onContainerStart(containerInfo: ContainerInfo) { + const message = `Container Started: ${containerInfo.name} on ${containerInfo.hostId}`; + try { + const response = await fetch( + `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + chat_id: TELEGRAM_CHAT_ID, + text: message, + }), + }, + ); + if (!response.ok) { + logger.error(`HTTP error ${response.status}`); + } + logger.info("Telegram notification sent."); + } catch (error) { + logger.error("Failed to send Telegram notification", error as string); + } + }, } satisfies Plugin; export default TelegramNotificationPlugin; diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index 0b2c4fb..5be018c 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -1,135 +1,257 @@ +import { existsSync, readdir, readdirSync, unlinkSync } from "node:fs"; import { Elysia, t } from "elysia"; - -import { logger } from "~/core/utils/logger"; import { dbFunctions } from "~/core/database"; import { pluginManager } from "~/core/plugins/plugin-manager"; -import { responseHandler } from "~/core/utils/response-handler"; +import { logger } from "~/core/utils/logger"; import { - version, - authorEmail, - authorName, - authorWebsite, - contributors, - dependencies, - description, - devDependencies, - license, + authorEmail, + authorName, + authorWebsite, + contributors, + dependencies, + description, + devDependencies, + license, + version, } from "~/core/utils/package-json"; +import { responseHandler } from "~/core/utils/response-handler"; +import { backupDir } from "~/core/database/backup"; import { hashApiKey } from "~/middleware/auth"; - -import { config } from "~/typings/database"; +import type { config } from "~/typings/database"; export const apiConfigRoutes = new Elysia({ prefix: "/config" }) - .get( - "/", - async ({ set }) => { - try { - const data = dbFunctions.getConfig() as config[]; - const distinct = data[0]; - set.status = 200; - set.headers["Content-Type"] = "application/json"; - logger.debug("Fetched backend config"); - return distinct; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Error getting the DockStatAPI config" - ); - } - }, - { - detail: { - tags: ["Management"], - description: - "Returns current API configuration including data retention policies and security settings", - }, - } - ) - .get( - "/plugins", - ({ set }) => { - try { - return pluginManager.getLoadedPlugins(); - } catch (error) { - return responseHandler.error( - set, - error as string, - "Error getting all registered plugins" - ); - } - }, - { - detail: { - tags: ["Management"], - description: - "Lists all active plugins with their registration details and status", - }, - } - ) - .post( - "/update", - async ({ set, body }) => { - try { - const { fetching_interval, keep_data_for, api_key } = body; - set.headers["Content-Type"] = "application/json"; - dbFunctions.updateConfig( - fetching_interval, - keep_data_for, - await hashApiKey(api_key) - ); - return responseHandler.ok(set, "Updated DockStatAPI config"); - } catch (error) { - return responseHandler.error( - set, - "Error updating the DockStatAPI config", - error as string - ); - } - }, - { - body: t.Object({ - fetching_interval: t.Number(), - keep_data_for: t.Number(), - api_key: t.String(), - }), - detail: { - tags: ["Management"], - description: - "Modifies core API settings including data collection intervals, retention periods, and security credentials", - }, - } - ) - .get( - "/package", - async ({ set }) => { - try { - logger.debug("Fetching package.json"); - return { - version: version, - description: description, - license: license, - authorName: authorName, - authorEmail: authorEmail, - authorWebsite: authorWebsite, - contributors: contributors, - dependencies: dependencies, - devDependencies: devDependencies, - }; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Error while reading package.json" - ); - } - }, - { - detail: { - tags: ["Management"], - description: - "Displays package metadata including dependencies, contributors, and licensing information", - }, - } - ); + .get( + "/", + async ({ set }) => { + try { + const data = dbFunctions.getConfig() as config[]; + const distinct = data[0]; + set.status = 200; + set.headers["Content-Type"] = "application/json"; + logger.debug("Fetched backend config"); + return distinct; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Error getting the DockStatAPI config", + ); + } + }, + { + detail: { + tags: ["Management"], + description: + "Returns current API configuration including data retention policies and security settings", + }, + }, + ) + .get( + "/plugins", + ({ set }) => { + try { + return pluginManager.getLoadedPlugins(); + } catch (error) { + return responseHandler.error( + set, + error as string, + "Error getting all registered plugins", + ); + } + }, + { + detail: { + tags: ["Management"], + description: + "Lists all active plugins with their registration details and status", + }, + }, + ) + .post( + "/update", + async ({ set, body }) => { + try { + const { fetching_interval, keep_data_for, api_key } = body; + set.headers["Content-Type"] = "application/json"; + dbFunctions.updateConfig( + fetching_interval, + keep_data_for, + await hashApiKey(api_key), + ); + return responseHandler.ok(set, "Updated DockStatAPI config"); + } catch (error) { + return responseHandler.error( + set, + "Error updating the DockStatAPI config", + error as string, + ); + } + }, + { + body: t.Object({ + fetching_interval: t.Number(), + keep_data_for: t.Number(), + api_key: t.String(), + }), + detail: { + tags: ["Management"], + description: + "Modifies core API settings including data collection intervals, retention periods, and security credentials", + }, + }, + ) + .get( + "/package", + async ({ set }) => { + try { + logger.debug("Fetching package.json"); + return { + version: version, + description: description, + license: license, + authorName: authorName, + authorEmail: authorEmail, + authorWebsite: authorWebsite, + contributors: contributors, + dependencies: dependencies, + devDependencies: devDependencies, + }; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Error while reading package.json", + ); + } + }, + { + detail: { + tags: ["Management"], + description: + "Displays package metadata including dependencies, contributors, and licensing information", + }, + }, + ) + .post( + "/backup", + async ({ set }) => { + try { + const backupFilename = await dbFunctions.backupDatabase(); + return responseHandler.ok(set, backupFilename); + } catch (error) { + return responseHandler.error(set, error as string, "Error backing up"); + } + }, + { + detail: { + tags: ["Management"], + description: "Backs up the internal database", + }, + }, + ) + .get( + "/backup", + async ({ set }) => { + try { + const backupFiles = readdirSync(backupDir); + + const filteredFiles = backupFiles.filter((file: string) => { + return !( + file.endsWith(".db") || + file.endsWith(".db-shm") || + file.endsWith(".db-wal") + ); + }); + + return filteredFiles; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Reading Backup directory", + ); + } + }, + { + detail: { + tags: ["Management"], + description: "Lists all available backups", + }, + }, + ) + + .get( + "/backup/download", + async ({ query, set }) => { + try { + const filename = query.filename || dbFunctions.findLatestBackup(); + const filePath = `${backupDir}/${filename}`; + + if (!existsSync(filePath)) { + throw new Error("Backup file not found"); + } + + set.headers["Content-Type"] = "application/octet-stream"; + set.headers["Content-Disposition"] = + `attachment; filename="${filename}"`; + return Bun.file(filePath); + } catch (error) { + return responseHandler.error( + set, + error as string, + "Backup download failed", + ); + } + }, + { + query: t.Object({ + filename: t.Optional(t.String()), + }), + detail: { + tags: ["Management"], + description: + "Download a specific backup or the latest if no filename is provided", + }, + }, + ) + .post( + "/restore", + async ({ body, set }) => { + try { + const { file } = body; + + set.headers["Content-Type"] = "text/html"; + + if (!file) { + throw new Error("No file uploaded"); + } + + if (!file.name.endsWith(".db.bak")) { + throw new Error("Invalid file type. Expected .db.bak"); + } + + const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; + const fileBuffer = await file.arrayBuffer(); + + await Bun.write(tempPath, fileBuffer); + dbFunctions.restoreDatabase(tempPath); + unlinkSync(tempPath); + + return responseHandler.ok(set, "Database restored successfully"); + } catch (error) { + return responseHandler.error( + set, + error instanceof Error ? error.message : "Restoration failed", + "Database restoration error", + ); + } + }, + { + body: t.Object({ file: t.File() }), + detail: { + tags: ["Management"], + description: "Restore database from uploaded backup file", + }, + }, + ); diff --git a/src/routes/docker-manager.ts b/src/routes/docker-manager.ts index 3574751..8caadd2 100644 --- a/src/routes/docker-manager.ts +++ b/src/routes/docker-manager.ts @@ -1,119 +1,119 @@ import { Elysia, t } from "elysia"; -import { logger } from "~/core/utils/logger"; import { dbFunctions } from "~/core/database"; +import { logger } from "~/core/utils/logger"; import { responseHandler } from "~/core/utils/response-handler"; -import { DockerHost } from "~/typings/docker"; +import type { DockerHost } from "~/typings/docker"; export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) - .post( - "/add-host", - async ({ set, body }) => { - try { - set.headers["Content-Type"] = "application/json"; - dbFunctions.addDockerHost(body as DockerHost); - return responseHandler.ok(set, `Added docker host (${body.name})`); - } catch (error: unknown) { - return responseHandler.error( - set, - "Error adding docker Host", - error as string - ); - } - }, - { - detail: { - tags: ["Management"], - description: - "Registers a new Docker host to the monitoring system with connection details", - }, - body: t.Object({ - name: t.String(), - hostAddress: t.String(), - secure: t.Boolean(), - }), - } - ) + .post( + "/add-host", + async ({ set, body }) => { + try { + set.headers["Content-Type"] = "application/json"; + dbFunctions.addDockerHost(body as DockerHost); + return responseHandler.ok(set, `Added docker host (${body.name})`); + } catch (error: unknown) { + return responseHandler.error( + set, + "Error adding docker Host", + error as string, + ); + } + }, + { + detail: { + tags: ["Management"], + description: + "Registers a new Docker host to the monitoring system with connection details", + }, + body: t.Object({ + name: t.String(), + hostAddress: t.String(), + secure: t.Boolean(), + }), + }, + ) - .post( - "/update-host", - async ({ set, body }) => { - try { - set.status = 200; - dbFunctions.updateDockerHost(body); - return responseHandler.ok(set, `Updated docker host (${body.id})`); - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to update host" - ); - } - }, - { - detail: { - tags: ["Management"], - description: - "Modifies existing Docker host configuration parameters (name, address, security)", - }, - body: t.Object({ - id: t.Number(), - name: t.String(), - hostAddress: t.String(), - secure: t.Boolean(), - }), - } - ) + .post( + "/update-host", + async ({ set, body }) => { + try { + set.status = 200; + dbFunctions.updateDockerHost(body); + return responseHandler.ok(set, `Updated docker host (${body.id})`); + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to update host", + ); + } + }, + { + detail: { + tags: ["Management"], + description: + "Modifies existing Docker host configuration parameters (name, address, security)", + }, + body: t.Object({ + id: t.Number(), + name: t.String(), + hostAddress: t.String(), + secure: t.Boolean(), + }), + }, + ) - .get( - "/hosts", - async ({ set }) => { - try { - const dockerHosts = dbFunctions.getDockerHosts(); - set.headers["Content-Type"] = "application/json"; - logger.debug("Retrieved docker hosts"); - return dockerHosts; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to retrieve hosts" - ); - } - }, - { - detail: { - tags: ["Management"], - description: - "Lists all configured Docker hosts with their connection settings", - }, - } - ) + .get( + "/hosts", + async ({ set }) => { + try { + const dockerHosts = dbFunctions.getDockerHosts(); + set.headers["Content-Type"] = "application/json"; + logger.debug("Retrieved docker hosts"); + return dockerHosts; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to retrieve hosts", + ); + } + }, + { + detail: { + tags: ["Management"], + description: + "Lists all configured Docker hosts with their connection settings", + }, + }, + ) - .delete( - "/hosts/:id", - async ({ set, params }) => { - try { - set.status = 200; - dbFunctions.deleteDockerHost(params.id); - return responseHandler.ok(set, `Deleted docker host (${params.id})`); - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to delete host" - ); - } - }, - { - detail: { - tags: ["Management"], - description: - "Removes Docker host from monitoring system and clears associated data", - }, - params: t.Object({ - id: t.Number(), - }), - } - ); + .delete( + "/hosts/:id", + async ({ set, params }) => { + try { + set.status = 200; + dbFunctions.deleteDockerHost(params.id); + return responseHandler.ok(set, `Deleted docker host (${params.id})`); + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to delete host", + ); + } + }, + { + detail: { + tags: ["Management"], + description: + "Removes Docker host from monitoring system and clears associated data", + }, + params: t.Object({ + id: t.Number(), + }), + }, + ); diff --git a/src/routes/docker-stats.ts b/src/routes/docker-stats.ts index 3c31c5c..d804afa 100644 --- a/src/routes/docker-stats.ts +++ b/src/routes/docker-stats.ts @@ -1,166 +1,166 @@ -import Docker from "dockerode"; +import type Docker from "dockerode"; import { Elysia } from "elysia"; -import { logger } from "~/core/utils/logger"; import { dbFunctions } from "~/core/database"; import { getDockerClient } from "~/core/docker/client"; -import { findObjectByKey } from "~/core/utils/helpers"; -import { responseHandler } from "~/core/utils/response-handler"; import { - calculateCpuPercent, - calculateMemoryUsage, + calculateCpuPercent, + calculateMemoryUsage, } from "~/core/utils/calculations"; +import { findObjectByKey } from "~/core/utils/helpers"; +import { logger } from "~/core/utils/logger"; +import { responseHandler } from "~/core/utils/response-handler"; -import type { DockerInfo } from "~/typings/dockerode"; import type { ContainerInfo, DockerHost, HostStats } from "~/typings/docker"; +import type { DockerInfo } from "~/typings/dockerode"; export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) - .get( - "/containers", - async ({ set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const containers: ContainerInfo[] = []; + .get( + "/containers", + async ({ set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + const containers: ContainerInfo[] = []; - await Promise.all( - hosts.map(async (host) => { - try { - const docker = getDockerClient(host); - try { - await docker.ping(); - } catch (pingError) { - return responseHandler.error( - set, - pingError as string, - "Docker host connection failed" - ); - } + await Promise.all( + hosts.map(async (host) => { + try { + const docker = getDockerClient(host); + try { + await docker.ping(); + } catch (pingError) { + return responseHandler.error( + set, + pingError as string, + "Docker host connection failed", + ); + } - const hostContainers = await docker.listContainers({ all: true }); + const hostContainers = await docker.listContainers({ all: true }); - await Promise.all( - hostContainers.map(async (containerInfo) => { - try { - const container = docker.getContainer(containerInfo.Id); - const stats = await new Promise( - (resolve, reject) => { - container.stats({ stream: false }, (error, stats) => { - if (error) { - return responseHandler.reject( - set, - reject, - "An error occurred", - error - ); - } - if (!stats) { - return responseHandler.reject( - set, - reject, - "No stats available" - ); - } - resolve(stats); - }); - } - ); + await Promise.all( + hostContainers.map(async (containerInfo) => { + try { + const container = docker.getContainer(containerInfo.Id); + const stats = await new Promise( + (resolve, reject) => { + container.stats({ stream: false }, (error, stats) => { + if (error) { + return responseHandler.reject( + set, + reject, + "An error occurred", + error, + ); + } + if (!stats) { + return responseHandler.reject( + set, + reject, + "No stats available", + ); + } + resolve(stats); + }); + }, + ); - containers.push({ - id: containerInfo.Id, - hostId: `${host.id}`, - name: containerInfo.Names[0].replace(/^\//, ""), - image: containerInfo.Image, - status: containerInfo.Status, - state: containerInfo.State, - cpuUsage: calculateCpuPercent(stats), - memoryUsage: calculateMemoryUsage(stats), - stats: stats, - info: containerInfo, - }); - } catch (containerError) { - logger.error( - "Error fetching container stats,", - containerError - ); - } - }) - ); - logger.debug(`Fetched stats for ${host.name}`); - } catch (hostError) { - logger.error("Error fetching containers for host,", hostError); - } - }) - ); + containers.push({ + id: containerInfo.Id, + hostId: `${host.id}`, + name: containerInfo.Names[0].replace(/^\//, ""), + image: containerInfo.Image, + status: containerInfo.Status, + state: containerInfo.State, + cpuUsage: calculateCpuPercent(stats), + memoryUsage: calculateMemoryUsage(stats), + stats: stats, + info: containerInfo, + }); + } catch (containerError) { + logger.error( + "Error fetching container stats,", + containerError, + ); + } + }), + ); + logger.debug(`Fetched stats for ${host.name}`); + } catch (hostError) { + logger.error("Error fetching containers for host,", hostError); + } + }), + ); - set.headers["Content-Type"] = "application/json"; - logger.debug("Fetched all containers across all hosts"); - return { containers }; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to retrieve containers" - ); - } - }, - { - detail: { - tags: ["Statistics"], - description: - "Collects real-time statistics for all Docker containers across monitored hosts, including CPU and memory utilization", - }, - } - ) + set.headers["Content-Type"] = "application/json"; + logger.debug("Fetched all containers across all hosts"); + return { containers }; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to retrieve containers", + ); + } + }, + { + detail: { + tags: ["Statistics"], + description: + "Collects real-time statistics for all Docker containers across monitored hosts, including CPU and memory utilization", + }, + }, + ) - .get( - "/hosts/:id", - async ({ params, set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const host = findObjectByKey(hosts, "name", params.id); - if (!host) { - return responseHandler.simple_error( - set, - `Host (${params.id}) not found` - ); - } + .get( + "/hosts/:id", + async ({ params, set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + const host = findObjectByKey(hosts, "name", params.id); + if (!host) { + return responseHandler.simple_error( + set, + `Host (${params.id}) not found`, + ); + } - const docker = getDockerClient(host); - const info: DockerInfo = await docker.info(); + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); - const config: HostStats = { - hostId: host.id as number, - hostName: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - labels: info.Labels, - images: info.Images, - containers: info.Containers, - containersPaused: info.ContainersPaused, - containersRunning: info.ContainersRunning, - containersStopped: info.ContainersStopped, - }; + const config: HostStats = { + hostId: host.id as number, + hostName: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; - set.headers["Content-Type"] = "application/json"; - logger.debug(`Fetched config for ${host.name}`); - return config; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to retrieve host config" - ); - } - }, - { - detail: { - tags: ["Statistics"], - description: - "Provides detailed system metrics and Docker runtime information for specified host", - }, - } - ); + set.headers["Content-Type"] = "application/json"; + logger.debug(`Fetched config for ${host.name}`); + return config; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to retrieve host config", + ); + } + }, + { + detail: { + tags: ["Statistics"], + description: + "Provides detailed system metrics and Docker runtime information for specified host", + }, + }, + ); diff --git a/src/routes/docker-websocket.ts b/src/routes/docker-websocket.ts index 3165eff..51aefcd 100644 --- a/src/routes/docker-websocket.ts +++ b/src/routes/docker-websocket.ts @@ -1,134 +1,136 @@ -import split2 from "split2"; +import type { Readable } from "node:stream"; import { Elysia } from "elysia"; -import type { Readable } from "stream"; import type { ElysiaWS } from "elysia/dist/ws"; +import split2 from "split2"; -import { logger } from "~/core/utils/logger"; import { dbFunctions } from "~/core/database"; import { getDockerClient } from "~/core/docker/client"; -import { responseHandler } from "~/core/utils/response-handler"; import { - calculateCpuPercent, - calculateMemoryUsage, + calculateCpuPercent, + calculateMemoryUsage, } from "~/core/utils/calculations"; +import { logger } from "~/core/utils/logger"; +import { responseHandler } from "~/core/utils/response-handler"; +//biome-ignore lint/suspicious/noExplicitAny: const activeDockerConnections = new Set>(); const connectionStreams = new Map< - ElysiaWS, - Array<{ statsStream: Readable; splitStream: ReturnType }> + //biome-ignore lint/suspicious/noExplicitAny: + ElysiaWS, + Array<{ statsStream: Readable; splitStream: ReturnType }> >(); export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( - "/stats", - { - async open(ws) { - activeDockerConnections.add(ws); - connectionStreams.set(ws, []); + "/stats", + { + async open(ws) { + activeDockerConnections.add(ws); + connectionStreams.set(ws, []); - ws.send(JSON.stringify({ message: "Connection established" })); - logger.info(`New Docker WebSocket established (${ws.id})`); + ws.send(JSON.stringify({ message: "Connection established" })); + logger.info(`New Docker WebSocket established (${ws.id})`); - try { - const hosts = dbFunctions.getDockerHosts(); - logger.debug(`Retrieved ${hosts.length} docker host(s)`); + try { + const hosts = dbFunctions.getDockerHosts(); + logger.debug(`Retrieved ${hosts.length} docker host(s)`); - for (const host of hosts) { - if (ws.readyState !== 1) { - break; - } + for (const host of hosts) { + if (ws.readyState !== 1) { + break; + } - const docker = getDockerClient(host); - await docker.ping(); - const containers = await docker.listContainers({ all: true }); - logger.debug( - `Found ${containers.length} containers on ${host.name} (id: ${host.id})` - ); + const docker = getDockerClient(host); + await docker.ping(); + const containers = await docker.listContainers({ all: true }); + logger.debug( + `Found ${containers.length} containers on ${host.name} (id: ${host.id})`, + ); - for (const containerInfo of containers) { - if (ws.readyState !== 1) { - break; - } + for (const containerInfo of containers) { + if (ws.readyState !== 1) { + break; + } - const container = docker.getContainer(containerInfo.Id); - const statsStream = (await container.stats({ - stream: true, - })) as Readable; - const splitStream = split2(); + const container = docker.getContainer(containerInfo.Id); + const statsStream = (await container.stats({ + stream: true, + })) as Readable; + const splitStream = split2(); - connectionStreams.get(ws)?.push({ statsStream, splitStream }); + connectionStreams.get(ws)?.push({ statsStream, splitStream }); - statsStream - .on("close", () => splitStream.destroy()) - .pipe(splitStream) - .on("data", (line: string) => { - if (ws.readyState !== 1 || !line) { - return; - } - try { - const stats = JSON.parse(line); - ws.send( - JSON.stringify({ - id: containerInfo.Id, - hostId: host.id, - name: containerInfo.Names[0].replace(/^\//, ""), - image: containerInfo.Image, - status: containerInfo.Status, - state: containerInfo.State, - cpuUsage: calculateCpuPercent(stats) || 0, - memoryUsage: calculateMemoryUsage(stats) || 0, - }) - ); - } catch (error) { - logger.error(`Parse error: ${error}`); - } - }) - .on("error", (error: Error) => { - logger.error(`Stream error: ${error}`); - statsStream.destroy(); - ws.send( - JSON.stringify({ - hostId: host.name, - containerId: containerInfo.Id, - error: `Stats stream error: ${error}`, - }) - ); - }); - } - } - } catch (error) { - logger.error(`Connection error: ${error}`); - ws.send( - JSON.stringify( - responseHandler.error( - { headers: {} }, - error as string, - "Docker connection failed", - 500 - ) - ) - ); - } - }, + statsStream + .on("close", () => splitStream.destroy()) + .pipe(splitStream) + .on("data", (line: string) => { + if (ws.readyState !== 1 || !line) { + return; + } + try { + const stats = JSON.parse(line); + ws.send( + JSON.stringify({ + id: containerInfo.Id, + hostId: host.id, + name: containerInfo.Names[0].replace(/^\//, ""), + image: containerInfo.Image, + status: containerInfo.Status, + state: containerInfo.State, + cpuUsage: calculateCpuPercent(stats) || 0, + memoryUsage: calculateMemoryUsage(stats) || 0, + }), + ); + } catch (error) { + logger.error(`Parse error: ${error}`); + } + }) + .on("error", (error: Error) => { + logger.error(`Stream error: ${error}`); + statsStream.destroy(); + ws.send( + JSON.stringify({ + hostId: host.name, + containerId: containerInfo.Id, + error: `Stats stream error: ${error}`, + }), + ); + }); + } + } + } catch (error) { + logger.error(`Connection error: ${error}`); + ws.send( + JSON.stringify( + responseHandler.error( + { headers: {} }, + error as string, + "Docker connection failed", + 500, + ), + ), + ); + } + }, - message(ws, message) { - if (message === "pong") ws.pong(); - }, + message(ws, message) { + if (message === "pong") ws.pong(); + }, - close(ws) { - logger.info(`Closing connection ${ws.id}`); - activeDockerConnections.delete(ws); + close(ws) { + logger.info(`Closing connection ${ws.id}`); + activeDockerConnections.delete(ws); - const streams = connectionStreams.get(ws) || []; - streams.forEach(({ statsStream, splitStream }) => { - try { - statsStream.unpipe(splitStream); - statsStream.destroy(); - splitStream.destroy(); - } catch (error) { - logger.error(`Cleanup error: ${error}`); - } - }); - connectionStreams.delete(ws); - }, - } + const streams = connectionStreams.get(ws) || []; + for (const { statsStream, splitStream } of streams) { + try { + statsStream.unpipe(splitStream); + statsStream.destroy(); + splitStream.destroy(); + } catch (error) { + logger.error(`Cleanup error: ${error}`); + } + } + connectionStreams.delete(ws); + }, + }, ); diff --git a/src/routes/live-logs.ts b/src/routes/live-logs.ts index 17c4c69..2e894b6 100644 --- a/src/routes/live-logs.ts +++ b/src/routes/live-logs.ts @@ -3,29 +3,30 @@ import type { ElysiaWS } from "elysia/dist/ws"; import { logger } from "~/core/utils/logger"; -import { log_message } from "~/typings/database"; +import type { log_message } from "~/typings/database"; +//biome-ignore lint/suspicious/noExplicitAny: const activeConnections = new Set>(); export const liveLogs = new Elysia({ prefix: "/logs" }).ws("/ws", { - open(ws) { - activeConnections.add(ws); - ws.send({ message: "Connection established" }); - logger.info(`New Logs WebSocket established (${ws.id})`); - }, - close(ws) { - logger.info(`Logs WebSocket closed (${ws.id})`); - activeConnections.delete(ws); - }, + open(ws) { + activeConnections.add(ws); + ws.send({ message: "Connection established" }); + logger.info(`New Logs WebSocket established (${ws.id})`); + }, + close(ws) { + logger.info(`Logs WebSocket closed (${ws.id})`); + activeConnections.delete(ws); + }, }); export function logToClients(data: log_message) { - activeConnections.forEach((ws) => { - try { - ws.send(JSON.stringify(data)); - } catch (error) { - activeConnections.delete(ws); - logger.error("Failed to send to WebSocket:", error); - } - }); + for (const ws of activeConnections) { + try { + ws.send(JSON.stringify(data)); + } catch (error) { + activeConnections.delete(ws); + logger.error("Failed to send to WebSocket:", error); + } + } } diff --git a/src/routes/live-stacks.ts b/src/routes/live-stacks.ts index 4f3d395..b3a14e7 100644 --- a/src/routes/live-stacks.ts +++ b/src/routes/live-stacks.ts @@ -2,29 +2,30 @@ import { Elysia } from "elysia"; import type { ElysiaWS } from "elysia/dist/ws"; import { logger } from "~/core/utils/logger"; -import { stackSocketMessage } from "~/typings/websocket"; +import type { stackSocketMessage } from "~/typings/websocket"; +//biome-ignore lint/suspicious/noExplicitAny: Any = Connections const activeConnections = new Set>(); export const liveStacks = new Elysia().ws("/stacks", { - open(ws) { - activeConnections.add(ws); - ws.send({ message: "Connection established" }); - logger.info(`New Stacks WebSocket established (${ws.id})`); - }, - close(ws) { - logger.info(`Stacks WebSocket closed (${ws.id})`); - activeConnections.delete(ws); - }, + open(ws) { + activeConnections.add(ws); + ws.send({ message: "Connection established" }); + logger.info(`New Stacks WebSocket established (${ws.id})`); + }, + close(ws) { + logger.info(`Stacks WebSocket closed (${ws.id})`); + activeConnections.delete(ws); + }, }); export function postToClient(data: stackSocketMessage) { - activeConnections.forEach((ws) => { - try { - ws.send(JSON.stringify(data)); - } catch (error) { - activeConnections.delete(ws); - logger.error("Failed to send to WebSocket:", error); - } - }); + for (const ws of activeConnections) { + try { + ws.send(JSON.stringify(data)); + } catch (error) { + activeConnections.delete(ws); + logger.error("Failed to send to WebSocket:", error); + } + } } diff --git a/src/routes/logs.ts b/src/routes/logs.ts index ce33235..626e723 100644 --- a/src/routes/logs.ts +++ b/src/routes/logs.ts @@ -1,95 +1,95 @@ import { Elysia } from "elysia"; -import { logger } from "~/core/utils/logger"; import { dbFunctions } from "~/core/database"; +import { logger } from "~/core/utils/logger"; export const backendLogs = new Elysia({ prefix: "/logs" }) - .get( - "/", - async ({ set }) => { - try { - const logs = dbFunctions.getAllLogs(); - set.headers["Content-Type"] = "application/json"; - logger.debug(`Retrieved all logs`); - return logs; - } catch (error) { - set.status = 500; - logger.error("Failed to retrieve logs,", error); - return { error: "Failed to retrieve logs" }; - } - }, - { - detail: { - tags: ["Management"], - description: - "Retrieves complete application log history from persistent storage", - }, - } - ) + .get( + "/", + async ({ set }) => { + try { + const logs = dbFunctions.getAllLogs(); + set.headers["Content-Type"] = "application/json"; + logger.debug("Retrieved all logs"); + return logs; + } catch (error) { + set.status = 500; + logger.error("Failed to retrieve logs,", error); + return { error: "Failed to retrieve logs" }; + } + }, + { + detail: { + tags: ["Management"], + description: + "Retrieves complete application log history from persistent storage", + }, + }, + ) - .get( - "/:level", - async ({ params: { level }, set }) => { - try { - const logs = dbFunctions.getLogsByLevel(level); - set.headers["Content-Type"] = "application/json"; - logger.debug(`Retrieved logs (level: ${level})`); - return logs; - } catch (error) { - set.status = 500; - logger.error("Failed to retrieve logs"); - return { error: "Failed to retrieve logs" }; - } - }, - { - detail: { - tags: ["Management"], - description: - "Filters logs by severity level (debug, info, warn, error, fatal)", - }, - } - ) + .get( + "/:level", + async ({ params: { level }, set }) => { + try { + const logs = dbFunctions.getLogsByLevel(level); + set.headers["Content-Type"] = "application/json"; + logger.debug(`Retrieved logs (level: ${level})`); + return logs; + } catch (error) { + set.status = 500; + logger.error("Failed to retrieve logs"); + return { error: "Failed to retrieve logs" }; + } + }, + { + detail: { + tags: ["Management"], + description: + "Filters logs by severity level (debug, info, warn, error, fatal)", + }, + }, + ) - .delete( - "/", - async ({ set }) => { - try { - set.status = 200; - set.headers["Content-Type"] = "application/json"; - dbFunctions.clearAllLogs(); - return { success: true }; - } catch (error) { - set.status = 500; - logger.error("Could not delete all logs,", error); - return { error: "Could not delete all logs" }; - } - }, - { - detail: { - tags: ["Management"], - description: "Purges all historical log records from the database", - }, - } - ) + .delete( + "/", + async ({ set }) => { + try { + set.status = 200; + set.headers["Content-Type"] = "application/json"; + dbFunctions.clearAllLogs(); + return { success: true }; + } catch (error) { + set.status = 500; + logger.error("Could not delete all logs,", error); + return { error: "Could not delete all logs" }; + } + }, + { + detail: { + tags: ["Management"], + description: "Purges all historical log records from the database", + }, + }, + ) - .delete( - "/:level", - async ({ params: { level }, set }) => { - try { - dbFunctions.clearLogsByLevel(level); - set.headers["Content-Type"] = "application/json"; - logger.debug(`Cleared all logs with level: ${level}`); - return { success: true }; - } catch (error) { - set.status = 500; - logger.error("Could not clear logs with level", level, ",", error); - return { error: "Failed to retrieve logs" }; - } - }, - { - detail: { - tags: ["Management"], - description: "Clears log entries matching specified severity level", - }, - } - ); + .delete( + "/:level", + async ({ params: { level }, set }) => { + try { + dbFunctions.clearLogsByLevel(level); + set.headers["Content-Type"] = "application/json"; + logger.debug(`Cleared all logs with level: ${level}`); + return { success: true }; + } catch (error) { + set.status = 500; + logger.error("Could not clear logs with level", level, ",", error); + return { error: "Failed to retrieve logs" }; + } + }, + { + detail: { + tags: ["Management"], + description: "Clears log entries matching specified severity level", + }, + }, + ); diff --git a/src/routes/stacks.ts b/src/routes/stacks.ts index 528a5c1..56c8d1b 100644 --- a/src/routes/stacks.ts +++ b/src/routes/stacks.ts @@ -1,302 +1,291 @@ import { Elysia, t } from "elysia"; -import { logger } from "~/core/utils/logger"; import { dbFunctions } from "~/core/database"; -import { responseHandler } from "~/core/utils/response-handler"; import { - deployStack, - stopStack, - pullStackImages, - restartStack, - getStackStatus, - startStack, - getAllStacksStatus, - removeStack, + deployStack, + getAllStacksStatus, + getStackStatus, + pullStackImages, + removeStack, + restartStack, + startStack, + stopStack, } from "~/core/stacks/controller"; +import { logger } from "~/core/utils/logger"; +import { responseHandler } from "~/core/utils/response-handler"; export const stackRoutes = new Elysia({ prefix: "/stacks" }) - .post( - "/deploy", - async ({ set, body }) => { - try { - const isCustom = body.isCustom || false; + .post( + "/deploy", + async ({ set, body }) => { + try { + const isCustom = body.isCustom || false; + + const image_updates = body.image_updates || false; + + const missingParams: string[] = []; + if (!body.compose_spec) { + missingParams.push("compose_spec"); + } + if (body.automatic_reboot_on_error === undefined) { + missingParams.push("automatic_reboot_on_error"); + } + if (!body.source) { + missingParams.push("source"); + } + if (!body.name) { + missingParams.push("name"); + } + + if (missingParams.length > 0) { + const errMsg = `Missing values of: ${missingParams.join("; ")}`; + return responseHandler.error(set, errMsg, errMsg); + } + + await deployStack( + body.compose_spec, + body.name, + body.version, + body.source, + body.automatic_reboot_on_error, + isCustom, + image_updates, + body.stack_prefix, + ); + logger.info(`Deployed Stack (${body.name})`); + return responseHandler.ok( + set, + `Stack ${body.name} deployed successfully`, + ); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + + return responseHandler.error(set, errorMsg, "Error deploying stack"); + } + }, + { + detail: { + tags: ["Stacks"], + description: + "Deploys a new Docker stack using a provided compose specification, allowing custom configurations and image updates", + }, + body: t.Object({ + compose_spec: t.Any(), + name: t.String(), + version: t.Number(), + automatic_reboot_on_error: t.Boolean(), + isCustom: t.Boolean(), + image_updates: t.Boolean(), + source: t.String(), + stack_prefix: t.Optional(t.String()), + }), + }, + ) + .post( + "/start", + async ({ set, body }) => { + try { + if (!body.stackId) { + throw new Error("Stack ID needed"); + } + await startStack(body.stackId); + logger.info(`Started Stack (${body.stackId})`); + return responseHandler.ok( + set, + `Stack ${body.stackId} started successfully`, + ); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + + return responseHandler.error(set, errorMsg, "Error starting stack"); + } + }, + { + detail: { + tags: ["Stacks"], + description: + "Initiates a Docker stack, starting all associated containers", + }, + body: t.Object({ + stackId: t.Number(), + }), + }, + ) + .post( + "/stop", + async ({ set, body }) => { + try { + if (!body.stackId) { + throw new Error("Stack needed"); + } + await stopStack(body.stackId); + logger.info(`Stopped Stack (${body.stackId})`); + return responseHandler.ok( + set, + `Stack ${body.stackId} stopped successfully`, + ); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + + return responseHandler.error(set, errorMsg, "Error stopping stack"); + } + }, + { + detail: { + tags: ["Stacks"], + description: + "Halts a running Docker stack and its containers while preserving configurations", + }, + body: t.Object({ + stackId: t.Number(), + }), + }, + ) + .post( + "/restart", + async ({ set, body }) => { + try { + if (!body.stackId) { + throw new Error("Stack needed"); + } + await restartStack(body.stackId); + logger.info(`Restarted Stack (${body.stackId})`); + return responseHandler.ok( + set, + `Stack ${body.stackId} restarted successfully`, + ); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + + return responseHandler.error(set, errorMsg, "Error restarting stack"); + } + }, + { + detail: { + tags: ["Stacks"], + description: + "Performs full stack restart - stops and restarts all stack components in sequence", + }, + body: t.Object({ + stackId: t.Number(), + }), + }, + ) + .post( + "/pull-images", + async ({ set, body }) => { + try { + if (!body.stackId) { + throw new Error("Stack needed"); + } + await pullStackImages(body.stackId); + logger.info(`Pulled Stack images (${body.stackId})`); + return responseHandler.ok( + set, + `Images for stack ${body.stackId} pulled successfully`, + ); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); - const image_updates = body.image_updates || false; + return responseHandler.error(set, errorMsg, "Error pulling images"); + } + }, + { + detail: { + tags: ["Stacks"], + description: + "Updates container images for a stack using Docker's pull mechanism (requires stack ID)", + }, + body: t.Object({ + stackId: t.Number(), + }), + }, + ) + .get( + "/status", + async ({ set, query }) => { + try { + //biome-ignore lint/suspicious/noExplicitAny: + let status: Record; + let res = {}; + if (query.stackId) { + status = await getStackStatus(query.stackId); + res = responseHandler.ok( + set, + `Stack ${query.stackId} status retrieved successfully`, + ); + logger.info("Fetched Stack status"); + } else { + status = await getAllStacksStatus(); + res = responseHandler.ok(set, "Fetched all Stack's status"); + logger.info("Fetched all Stack status"); + } + return { ...res, status: status }; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); - let missingParams: string[] = []; - if (!body.compose_spec) { - missingParams.push("compose_spec"); - } - if (body.automatic_reboot_on_error === undefined) { - missingParams.push("automatic_reboot_on_error"); - } - if (!body.source) { - missingParams.push("source"); - } - if (!body.name) { - missingParams.push("name"); - } + return responseHandler.error( + set, + errorMsg, + "Error getting stack status", + ); + } + }, + { + detail: { + tags: ["Stacks"], + description: + "Retrieves operational status for either a specific stack (by ID) or all managed stacks", + }, + query: t.Object({ + stackId: t.Number(), + }), + }, + ) + .get( + "/", + async ({ set }) => { + try { + const stacks = dbFunctions.getStacks(); + logger.info("Fetched Stacks"); + return stacks; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); - if (missingParams.length > 0) { - const errMsg = `Missing values of: ${missingParams.join("; ")}`; - return responseHandler.error(set, errMsg, errMsg); - } + return responseHandler.error(set, errorMsg, "Error getting stacks"); + } + }, + { + detail: { + tags: ["Stacks"], + description: + "Lists all registered stacks with their complete configuration details", + }, + }, + ) - await deployStack( - body.compose_spec, - body.name, - body.version, - body.source, - body.automatic_reboot_on_error, - isCustom, - image_updates, - body.stack_prefix - ); - logger.info(`Deployed Stack (${body.name})`); - return responseHandler.ok( - set, - `Stack ${body.name} deployed successfully` - ); - } catch (error: any) { - return responseHandler.error( - set, - error.message || error, - "Error deploying stack" - ); - } - }, - { - detail: { - tags: ["Stacks"], - description: - "Deploys a new Docker stack using a provided compose specification, allowing custom configurations and image updates", - }, - body: t.Object({ - compose_spec: t.Any(), - name: t.String(), - version: t.Number(), - automatic_reboot_on_error: t.Boolean(), - isCustom: t.Boolean(), - image_updates: t.Boolean(), - source: t.String(), - stack_prefix: t.Optional(t.String()), - }), - } - ) - .post( - "/start", - async ({ set, body }) => { - try { - if (!body.stackId) { - throw new Error("Stack ID needed"); - } - await startStack(body.stackId); - logger.info(`Started Stack (${body.stackId})`); - return responseHandler.ok( - set, - `Stack ${body.stackId} started successfully` - ); - } catch (error: any) { - return responseHandler.error( - set, - error.message || error, - "Error starting stack" - ); - } - }, - { - detail: { - tags: ["Stacks"], - description: - "Initiates a Docker stack, starting all associated containers", - }, - body: t.Object({ - stackId: t.Number(), - }), - } - ) - .post( - "/stop", - async ({ set, body }) => { - try { - if (!body.stackId) { - throw new Error("Stack needed"); - } - await stopStack(body.stackId); - logger.info(`Stopped Stack (${body.stackId})`); - return responseHandler.ok( - set, - `Stack ${body.stackId} stopped successfully` - ); - } catch (error: any) { - return responseHandler.error( - set, - error.message || error, - "Error stopping stack" - ); - } - }, - { - detail: { - tags: ["Stacks"], - description: - "Halts a running Docker stack and its containers while preserving configurations", - }, - body: t.Object({ - stackId: t.Number(), - }), - } - ) - .post( - "/restart", - async ({ set, body }) => { - try { - if (!body.stackId) { - throw new Error("Stack needed"); - } - await restartStack(body.stackId); - logger.info(`Restarted Stack (${body.stackId})`); - return responseHandler.ok( - set, - `Stack ${body.stackId} restarted successfully` - ); - } catch (error: any) { - return responseHandler.error( - set, - error.message || error, - "Error restarting stack" - ); - } - }, - { - detail: { - tags: ["Stacks"], - description: - "Performs full stack restart - stops and restarts all stack components in sequence", - }, - body: t.Object({ - stackId: t.Number(), - }), - } - ) - .post( - "/pull-images", - async ({ set, body }) => { - try { - if (!body.stackId) { - throw new Error("Stack needed"); - } - await pullStackImages(body.stackId); - logger.info(`Pulled Stack images (${body.stackId})`); - return responseHandler.ok( - set, - `Images for stack ${body.stackId} pulled successfully` - ); - } catch (error: any) { - return responseHandler.error( - set, - error.message || error, - "Error pulling images" - ); - } - }, - { - detail: { - tags: ["Stacks"], - description: - "Updates container images for a stack using Docker's pull mechanism (requires stack ID)", - }, - body: t.Object({ - stackId: t.Number(), - }), - } - ) - .get( - "/status", - async ({ set, query }) => { - try { - let status; - let res = {}; - if (query.stackId) { - status = await getStackStatus(query.stackId); - res = responseHandler.ok( - set, - `Stack ${query.stackId} status retrieved successfully` - ); - logger.info("Fetched Stack status"); - } else { - status = await getAllStacksStatus(); - res = responseHandler.ok(set, "Fetched all Stack's status"); - logger.info("Fetched all Stack status"); - } - return { ...res, status: status }; - } catch (error: any) { - return responseHandler.error( - set, - error.message || error, - "Error getting stack status" - ); - } - }, - { - detail: { - tags: ["Stacks"], - description: - "Retrieves operational status for either a specific stack (by ID) or all managed stacks", - }, - query: t.Object({ - stackId: t.Number(), - }), - } - ) - .get( - "/", - async ({ set }) => { - try { - const stacks = dbFunctions.getStacks(); - logger.info("Fetched Stacks"); - return stacks; - } catch (error: any) { - return responseHandler.error( - set, - error.message || error, - "Error getting stacks" - ); - } - }, - { - detail: { - tags: ["Stacks"], - description: - "Lists all registered stacks with their complete configuration details", - }, - } - ) + .delete( + "/", + async ({ set, body }) => { + try { + const { stackId } = body; + await removeStack(stackId); + logger.info(`Deleted Stack ${stackId}`); + return responseHandler.ok(set, `Stack ${stackId} deleted successfully`); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); - .delete( - "/", - async ({ set, body }) => { - try { - const { stackId } = body; - await removeStack(stackId); - logger.info(`Deleted Stack ${stackId}`); - return responseHandler.ok(set, `Stack ${stackId} deleted successfully`); - } catch (error: any) { - return responseHandler.error( - set, - error.message || error, - "Error deleting stack" - ); - } - }, - { - detail: { - tags: ["Stacks"], - description: - "Permanently removes a stack configuration and cleans up associated resources", - }, - body: t.Object({ - stackId: t.Number(), - }), - } - ); + return responseHandler.error(set, errorMsg, "Error deleting stack"); + } + }, + { + detail: { + tags: ["Stacks"], + description: + "Permanently removes a stack configuration and cleans up associated resources", + }, + body: t.Object({ + stackId: t.Number(), + }), + }, + ); diff --git a/src/routes/utils.ts b/src/routes/utils.ts index 17cba24..b578f92 100644 --- a/src/routes/utils.ts +++ b/src/routes/utils.ts @@ -1,47 +1,47 @@ import { Elysia, t } from "elysia"; -import { responseHandler } from "~/core/utils/response-handler"; import { - version, - authorEmail, - authorName, - authorWebsite, - contributors, - dependencies, - description, - devDependencies, - license, + authorEmail, + authorName, + authorWebsite, + contributors, + dependencies, + description, + devDependencies, + license, + version, } from "~/core/utils/package-json"; +import { responseHandler } from "~/core/utils/response-handler"; export const utilRoutes = new Elysia({ prefix: "/utils" }).get( - "/info", - async ({ set }) => { - try { - set.status = 200; - return { - version, - authorEmail, - authorName, - authorWebsite, - contributors, - dependencies, - description, - devDependencies, - license, - }; - } catch (error: any) { - return responseHandler.error( - set, - error.message || error, - "Error getting DockStatAPI information" - ); - } - }, - { - detail: { - tags: ["Utils"], - description: - "Retrieves DockStatAPI metadata including version, author information, dependencies, and licensing details", - }, - } + "/info", + async ({ set }) => { + try { + set.status = 200; + return { + version, + authorEmail, + authorName, + authorWebsite, + contributors, + dependencies, + description, + devDependencies, + license, + }; + } catch (error) { + return responseHandler.error( + set, + String(error), + "Error getting DockStatAPI information", + ); + } + }, + { + detail: { + tags: ["Utils"], + description: + "Retrieves DockStatAPI metadata including version, author information, dependencies, and licensing details", + }, + }, ); diff --git a/src/tests/cleanup.ts b/src/tests/cleanup.ts index 31756dd..ac90de7 100644 --- a/src/tests/cleanup.ts +++ b/src/tests/cleanup.ts @@ -6,15 +6,15 @@ import type { DockerHost } from "~/typings/docker"; console.log(""); console.log("Deleting `test` Docker host"); -let testHosts: DockerHost[] = dbFunctions.getDockerHosts(); +const testHosts: DockerHost[] = dbFunctions.getDockerHosts(); const testHost = findObjectByKey(testHosts, "name", "test"); if (testHost) { - dbFunctions.deleteDockerHost(testHost.id as number); - console.log(`Docker host with name "${testHost.name}" deleted.`); + dbFunctions.deleteDockerHost(testHost.id as number); + console.log(`Docker host with name "${testHost.name}" deleted.`); } else { - console.log("Docker host not found."); + console.log("Docker host not found."); } console.log("Cleaning up Database config to default values"); diff --git a/src/tests/delete.spec.ts b/src/tests/delete.spec.ts index 097ede0..901b05f 100644 --- a/src/tests/delete.spec.ts +++ b/src/tests/delete.spec.ts @@ -3,11 +3,11 @@ import { describe, it } from "bun:test"; import { runTestCode } from "./helper"; describe("DockStatAPI (DELETE)", () => { - it("Delete all Logs /logs", async () => { - await runTestCode("/logs", 200, "DELETE", {}); - }); + it("Delete all Logs /logs", async () => { + await runTestCode("/logs", 200, "DELETE", {}); + }); - it("Delete Logs (Debug) /logs/debug", async () => { - await runTestCode("/logs/debug", 200, "DELETE", {}); - }); + it("Delete Logs (Debug) /logs/debug", async () => { + await runTestCode("/logs/debug", 200, "DELETE", {}); + }); }); diff --git a/src/tests/gets.spec.ts b/src/tests/gets.spec.ts index 7b10315..e542a0f 100644 --- a/src/tests/gets.spec.ts +++ b/src/tests/gets.spec.ts @@ -1,61 +1,61 @@ import { describe, it } from "bun:test"; import { - version, - authorEmail, - authorName, - authorWebsite, - contributors, - dependencies, - description, - devDependencies, - license, + authorEmail, + authorName, + authorWebsite, + contributors, + dependencies, + description, + devDependencies, + license, + version, } from "~/core/utils/package-json"; -import { runTestResponse, runTestCode } from "./helper"; +import { runTestCode, runTestResponse } from "./helper"; describe("DockStatAPI (GET)", () => { - it("Check Server connection", async () => { - await runTestResponse("/health", '{"status":"healthy"}', "GET"); - }); - - it("Check /docker/containers", async () => { - await runTestCode("/docker/containers", 200, "GET"); - }); - - it("Check /docker/hosts/Localhost", async () => { - await runTestCode("/docker/hosts/Localhost", 200, "GET"); - }); - - it("Check /docker-config/hosts", async () => { - await runTestCode("/docker-config/hosts", 200, "GET"); - }); - - it("Check /logs/", async () => { - await runTestCode("/logs", 200, "GET"); - }); - - it("Check /logs/debug", async () => { - await runTestCode("/logs/debug", 200, "GET"); - }); - - it("Check /config", async () => { - await runTestCode("/config", 200, "GET"); - }); - - it("Check /config/package", async () => { - const expected = JSON.stringify({ - version, - description, - license, - authorName, - authorEmail, - authorWebsite, - contributors, - dependencies, - devDependencies, - }); - - await runTestResponse("/config/package", expected, "GET"); - }); + it("Check Server connection", async () => { + await runTestResponse("/health", '{"status":"healthy"}', "GET"); + }); + + it("Check /docker/containers", async () => { + await runTestCode("/docker/containers", 200, "GET"); + }); + + it("Check /docker/hosts/Localhost", async () => { + await runTestCode("/docker/hosts/Localhost", 200, "GET"); + }); + + it("Check /docker-config/hosts", async () => { + await runTestCode("/docker-config/hosts", 200, "GET"); + }); + + it("Check /logs/", async () => { + await runTestCode("/logs", 200, "GET"); + }); + + it("Check /logs/debug", async () => { + await runTestCode("/logs/debug", 200, "GET"); + }); + + it("Check /config", async () => { + await runTestCode("/config", 200, "GET"); + }); + + it("Check /config/package", async () => { + const expected = JSON.stringify({ + version, + description, + license, + authorName, + authorEmail, + authorWebsite, + contributors, + dependencies, + devDependencies, + }); + + await runTestResponse("/config/package", expected, "GET"); + }); }); diff --git a/src/tests/helper.ts b/src/tests/helper.ts index 59ec039..fabc45b 100644 --- a/src/tests/helper.ts +++ b/src/tests/helper.ts @@ -8,114 +8,119 @@ export const API_KEY = "TestKey"; const server = "http://localhost:3001"; export async function runTestResponse( - path: string, - expected_response: any, - method?: "GET" | "POST" | "DELETE", - requestBody?: any + path: string, + expected_response: string, + method: "GET" | "POST" | "DELETE" = "GET", + requestBody?: string, ) { - method = method || "GET"; - const route = `${server}${path}`; - - logger.info(`__UT__ [ START ] Running test, method: ${method} on ${route}`); - const startTime = Date.now(); - - try { - const processedBody = - requestBody !== undefined - ? typeof requestBody === "string" - ? requestBody - : JSON.stringify(requestBody) - : undefined; - - const request = new Request(route, { - method, - body: processedBody, - headers: { - "Content-Type": "application/json", - "x-api-key": API_KEY, - }, - }); - - logger.debug( - `Request details: ${JSON.stringify({ - url: route, - method, - headers: [...request.headers], - body: processedBody, - })}` - ); - - const response = await DockStatAPI.handle(request); - const headers: { [key: string]: string } = {}; - response.headers.forEach((value, key) => (headers[key] = value)); - - const responseText = await response.text(); - const duration = Date.now() - startTime; - - logger.debug(`Received HTTP status: ${response.status}`); - logger.debug(`Response headers: ${JSON.stringify(headers)}`); - logger.debug(`Response body: ${responseText}`); - logger.debug(`Total Duration: ${duration}ms`); - - expect(responseText).toBe(expected_response); - logger.info(`__UT__ [ END ] Completed test on ${route}`); - } catch (error) { - logger.error(`__UT__ Error during test on ${route}: ${error}`); - throw error; - } + const route = `${server}${path}`; + + logger.info(`__UT__ [ START ] Running test, method: ${method} on ${route}`); + const startTime = Date.now(); + + try { + const processedBody = + requestBody !== undefined + ? typeof requestBody === "string" + ? requestBody + : JSON.stringify(requestBody) + : undefined; + + const request = new Request(route, { + method, + body: processedBody, + headers: { + "Content-Type": "application/json", + "x-api-key": API_KEY, + }, + }); + + logger.debug( + `Request details: ${JSON.stringify({ + url: route, + method, + headers: [...request.headers], + body: processedBody, + })}`, + ); + + const response = await DockStatAPI.handle(request); + const headers: { [key: string]: string } = {}; + + response.headers.forEach((value, key) => { + headers[key] = value; + }); + + const responseText = await response.text(); + const duration = Date.now() - startTime; + + logger.debug(`Received HTTP status: ${response.status}`); + logger.debug(`Response headers: ${JSON.stringify(headers)}`); + logger.debug(`Response body: ${responseText}`); + logger.debug(`Total Duration: ${duration}ms`); + + expect(responseText).toBe(expected_response); + logger.info(`__UT__ [ END ] Completed test on ${route}`); + } catch (error) { + logger.error(`__UT__ Error during test on ${route}: ${error}`); + throw error; + } } export async function runTestCode( - path: string, - expected_code: number, - method?: "GET" | "POST" | "DELETE", - requestBody?: any + path: string, + expected_code: number, + method: "GET" | "POST" | "DELETE" = "GET", + requestBody?: object, ) { - method = method || "GET"; - const route = `${server}${path}`; - - logger.info(`__UT__ [ START ] Running test, method: ${method} on ${route}`); - const startTime = Date.now(); - - try { - const processedBody = - requestBody !== undefined - ? typeof requestBody === "string" - ? requestBody - : JSON.stringify(requestBody) - : undefined; - - const request = new Request(route, { - method, - body: processedBody, - headers: { - "Content-Type": "application/json", - "x-api-key": API_KEY, - }, - }); - - logger.debug( - `Request details: ${JSON.stringify({ - url: route, - method, - headers: [...request.headers], - body: processedBody, - })}` - ); - - const response = await DockStatAPI.handle(request); - const headers: { [key: string]: string } = {}; - response.headers.forEach((value, key) => (headers[key] = value)); - const duration = Date.now() - startTime; - - logger.debug(`Received HTTP status: ${response.status}`); - logger.debug(`Response headers: ${JSON.stringify(headers)}`); - logger.debug(`Response body: ${JSON.stringify(response.body)}`); - - expect(response.status).toBe(expected_code); - logger.debug(`__UT__ Completed test on ${route} (Duration: ${duration}ms)`); - } catch (error) { - logger.error(`__UT__ Error during test on ${route}: ${error}`); - throw error; - } + const route = `${server}${path}`; + + logger.info(`__UT__ [ START ] Running test, method: ${method} on ${route}`); + const startTime = Date.now(); + + try { + const processedBody = + requestBody !== undefined + ? typeof requestBody === "string" + ? requestBody + : JSON.stringify(requestBody) + : undefined; + + const request = new Request(route, { + method, + body: processedBody, + headers: { + "Content-Type": "application/json", + "x-api-key": API_KEY, + }, + }); + + logger.debug( + `Request details: ${JSON.stringify({ + url: route, + method, + headers: [...request.headers], + body: processedBody, + })}`, + ); + + const response = await DockStatAPI.handle(request); + const headers: { [key: string]: string } = {}; + + response.headers.forEach((value, key) => { + headers[key] = value; + }); + + const duration = Date.now() - startTime; + + logger.debug(`Received HTTP status: ${response.status}`); + logger.debug(`Response headers: ${JSON.stringify(headers)}`); + logger.debug(`Response body: ${JSON.stringify(response.body)}`); + + expect(response.status).toBe(expected_code); + logger.debug(`__UT__ Completed test on ${route} (Duration: ${duration}ms)`); + } catch (error) { + logger.error(`__UT__ Error during test on ${route}: ${error}`); + throw error; + } } diff --git a/src/tests/post.spec.ts b/src/tests/post.spec.ts index 9747e67..a4933dc 100644 --- a/src/tests/post.spec.ts +++ b/src/tests/post.spec.ts @@ -1,52 +1,52 @@ import { describe, it } from "bun:test"; -import { runTestResponse, runTestCode } from "./helper"; +import { runTestCode, runTestResponse } from "./helper"; -import { DockerHost } from "~/typings/docker"; +import type { DockerHost } from "~/typings/docker"; describe("DockStatAPI (POST)", () => { - it("Check Host adding", async () => { - const body = { - name: "test", - hostAddress: "localhost:2375", - secure: false, - }; - - await runTestCode("/docker-config/add-host", 200, "POST", body); - await runTestCode("/docker-config/hosts", 200, "GET"); - }); - - it("Check Host Updating", async () => { - const codeBody: DockerHost = { - id: 2, - name: "test", - hostAddress: "127.0.0.1:2375", - secure: false, - }; - - await runTestCode("/docker-config/update-host", 200, "POST", codeBody); - - const responseBody: DockerHost[] = [ - { id: 2, name: "test", hostAddress: "127.0.0.1:2375", secure: false }, - { - id: 1, - name: "Localhost", - hostAddress: "localhost:2375", - secure: false, - }, - ]; - await runTestResponse( - "/docker-config/hosts", - JSON.stringify(responseBody), - "GET" - ); - }); - - it("Check Config update", async () => { - await runTestCode("/config/update", 200, "POST", { - fetching_interval: 1, - keep_data_for: 1, - api_key: "TestKey", - }); - }); + it("Check Host adding", async () => { + const body = { + name: "test", + hostAddress: "localhost:2375", + secure: false, + }; + + await runTestCode("/docker-config/add-host", 200, "POST", body); + await runTestCode("/docker-config/hosts", 200, "GET"); + }); + + it("Check Host Updating", async () => { + const codeBody: DockerHost = { + id: 2, + name: "test", + hostAddress: "127.0.0.1:2375", + secure: false, + }; + + await runTestCode("/docker-config/update-host", 200, "POST", codeBody); + + const responseBody: DockerHost[] = [ + { id: 2, name: "test", hostAddress: "127.0.0.1:2375", secure: false }, + { + id: 1, + name: "Localhost", + hostAddress: "localhost:2375", + secure: false, + }, + ]; + await runTestResponse( + "/docker-config/hosts", + JSON.stringify(responseBody), + "GET", + ); + }); + + it("Check Config update", async () => { + await runTestCode("/config/update", 200, "POST", { + fetching_interval: 1, + keep_data_for: 1, + api_key: "TestKey", + }); + }); }); diff --git a/src/typings/database.ts b/src/typings/database.ts index 67d8121..880a801 100644 --- a/src/typings/database.ts +++ b/src/typings/database.ts @@ -1,27 +1,27 @@ interface config { - keep_data_for: number; - fetching_interval: number; - api_key: string; + keep_data_for: number; + fetching_interval: number; + api_key: string; } interface stacks_config { - id: number; - name: string; - version: number; - custom: boolean; - source: string; - container_count: number; - stack_prefix: string; - automatic_reboot_on_error: boolean; - image_updates: boolean; + id: number; + name: string; + version: number; + custom: boolean; + source: string; + container_count: number; + stack_prefix: string; + automatic_reboot_on_error: boolean; + image_updates: boolean; } interface log_message { - level: string; - timestamp: string; - message: string; - file: string; - line: number; + level: string; + timestamp: string; + message: string; + file: string; + line: number; } export type { config, stacks_config, log_message }; diff --git a/src/typings/docker-compose.ts b/src/typings/docker-compose.ts index 9067aba..8e6a5f9 100644 --- a/src/typings/docker-compose.ts +++ b/src/typings/docker-compose.ts @@ -1,440 +1,522 @@ export interface Stack { - compose_spec: ComposeSpec; - name: string; - version: number; - source: string; - id?: number; + compose_spec: ComposeSpec; + name: string; + version: number; + source: string; + id?: number; } export interface ComposeSpec { - version?: string; - name?: string; - include?: Include[]; - services?: { [key: string]: Service }; - networks?: { [key: string]: Network }; - volumes?: { [key: string]: Volume }; - secrets?: { [key: string]: Secret }; - configs?: { [key: string]: Config }; - [key: `x-${string}`]: any; + version?: string; + name?: string; + include?: Include[]; + services?: { [key: string]: Service }; + networks?: { [key: string]: Network }; + volumes?: { [key: string]: Volume }; + secrets?: { [key: string]: Secret }; + configs?: { [key: string]: Config }; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; } type Include = - | string - | { - path: string | string[]; - env_file?: string | string[]; - project_directory?: string; - }; + | string + | { + path: string | string[]; + env_file?: string | string[]; + project_directory?: string; + }; interface Service { - develop?: Development | null; - deploy?: Deployment | null; - annotations?: ListOrDict; - attach?: boolean | string; - build?: - | string - | { - context?: string; - dockerfile?: string; - dockerfile_inline?: string; - entitlements?: string[]; - args?: ListOrDict; - ssh?: ListOrDict; - labels?: ListOrDict; - cache_from?: string[]; - cache_to?: string[]; - no_cache?: boolean | string; - additional_contexts?: ListOrDict; - network?: string; - pull?: boolean | string; - target?: string; - shm_size?: number | string; - extra_hosts?: ExtraHosts; - isolation?: string; - privileged?: boolean | string; - secrets?: ServiceConfigOrSecret[]; - tags?: string[]; - ulimits?: Ulimits; - platforms?: string[]; - [key: `x-${string}`]: any; - }; - blkio_config?: { - device_read_bps?: BlkioLimit[]; - device_read_iops?: BlkioLimit[]; - device_write_bps?: BlkioLimit[]; - device_write_iops?: BlkioLimit[]; - weight?: number | string; - weight_device?: BlkioWeight[]; - }; - cap_add?: string[]; - cap_drop?: string[]; - cgroup?: "host" | "private"; - cgroup_parent?: string; - command?: Command; - configs?: ServiceConfigOrSecret[]; - container_name?: string; - cpu_count?: string | number; - cpu_percent?: string | number; - cpu_shares?: number | string; - cpu_quota?: number | string; - cpu_period?: number | string; - cpu_rt_period?: number | string; - cpu_rt_runtime?: number | string; - cpus?: number | string; - cpuset?: string; - credential_spec?: { - config?: string; - file?: string; - registry?: string; - [key: `x-${string}`]: any; - }; - depends_on?: - | string[] - | { - [service: string]: { - condition: - | "service_started" - | "service_healthy" - | "service_completed_successfully"; - restart?: boolean | string; - required?: boolean; - [key: `x-${string}`]: any; - }; - }; - device_cgroup_rules?: string[]; - devices?: ( - | string - | { - source: string; - target?: string; - permissions?: string; - [key: `x-${string}`]: any; - } - )[]; - dns?: StringOrList; - dns_opt?: string[]; - dns_search?: StringOrList; - domainname?: string; - entrypoint?: Command; - env_file?: EnvFile; - label_file?: string | string[]; - environment?: ListOrDict; - expose?: (string | number)[]; - extends?: string | { service: string; file?: string }; - external_links?: string[]; - extra_hosts?: ExtraHosts; - gpus?: - | "all" - | Array<{ - capabilities?: string[]; - count?: string | number; - device_ids?: string[]; - driver?: string; - options?: ListOrDict; - [key: `x-${string}`]: any; - }>; - group_add?: (string | number)[]; - healthcheck?: Healthcheck; - hostname?: string; - image?: string; - init?: boolean | string; - ipc?: string; - isolation?: string; - labels?: ListOrDict; - links?: string[]; - logging?: { - driver?: string; - options?: { [key: string]: string | number | null }; - [key: `x-${string}`]: any; - }; - mac_address?: string; - mem_limit?: number | string; - mem_reservation?: string | number; - mem_swappiness?: number | string; - memswap_limit?: number | string; - network_mode?: string; - networks?: - | string[] - | { - [network: string]: { - aliases?: string[]; - ipv4_address?: string; - ipv6_address?: string; - link_local_ips?: string[]; - mac_address?: string; - driver_opts?: { [key: string]: string | number }; - priority?: number; - [key: `x-${string}`]: any; - } | null; - }; - oom_kill_disable?: boolean | string; - oom_score_adj?: string | number; - pid?: string | null; - pids_limit?: number | string; - platform?: string; - ports?: ( - | number - | string - | { - name?: string; - mode?: string; - host_ip?: string; - target?: number | string; - published?: string | number; - protocol?: string; - app_protocol?: string; - [key: `x-${string}`]: any; - } - )[]; - post_start?: ServiceHook[]; - pre_stop?: ServiceHook[]; - privileged?: boolean | string; - profiles?: string[]; - pull_policy?: "always" | "never" | "if_not_present" | "build" | "missing"; - read_only?: boolean | string; - restart?: string; - runtime?: string; - scale?: number | string; - security_opt?: string[]; - shm_size?: number | string; - secrets?: ServiceConfigOrSecret[]; - sysctls?: ListOrDict; - stdin_open?: boolean | string; - stop_grace_period?: string; - stop_signal?: string; - storage_opt?: object; - tmpfs?: StringOrList; - tty?: boolean | string; - ulimits?: Ulimits; - user?: string; - uts?: string; - userns_mode?: string; - volumes?: ( - | string - | { - type: string; - source?: string; - target?: string; - read_only?: boolean | string; - consistency?: string; - bind?: { - propagation?: string; - create_host_path?: boolean | string; - recursive?: "enabled" | "disabled" | "writable" | "readonly"; - selinux?: "z" | "Z"; - [key: `x-${string}`]: any; - }; - volume?: { - nocopy?: boolean | string; - subpath?: string; - [key: `x-${string}`]: any; - }; - tmpfs?: { - size?: number | string; - mode?: number | string; - [key: `x-${string}`]: any; - }; - [key: `x-${string}`]: any; - } - )[]; - volumes_from?: string[]; - working_dir?: string; - [key: `x-${string}`]: any; + develop?: Development | null; + deploy?: Deployment | null; + annotations?: ListOrDict; + attach?: boolean | string; + build?: + | string + | { + context?: string; + dockerfile?: string; + dockerfile_inline?: string; + entitlements?: string[]; + args?: ListOrDict; + ssh?: ListOrDict; + labels?: ListOrDict; + cache_from?: string[]; + cache_to?: string[]; + no_cache?: boolean | string; + additional_contexts?: ListOrDict; + network?: string; + pull?: boolean | string; + target?: string; + shm_size?: number | string; + extra_hosts?: ExtraHosts; + isolation?: string; + privileged?: boolean | string; + secrets?: ServiceConfigOrSecret[]; + tags?: string[]; + ulimits?: Ulimits; + platforms?: string[]; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }; + blkio_config?: { + device_read_bps?: BlkioLimit[]; + device_read_iops?: BlkioLimit[]; + device_write_bps?: BlkioLimit[]; + device_write_iops?: BlkioLimit[]; + weight?: number | string; + weight_device?: BlkioWeight[]; + }; + cap_add?: string[]; + cap_drop?: string[]; + cgroup?: "host" | "private"; + cgroup_parent?: string; + command?: Command; + configs?: ServiceConfigOrSecret[]; + container_name?: string; + cpu_count?: string | number; + cpu_percent?: string | number; + cpu_shares?: number | string; + cpu_quota?: number | string; + cpu_period?: number | string; + cpu_rt_period?: number | string; + cpu_rt_runtime?: number | string; + cpus?: number | string; + cpuset?: string; + credential_spec?: { + config?: string; + file?: string; + registry?: string; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }; + depends_on?: + | string[] + | { + [service: string]: { + condition: + | "service_started" + | "service_healthy" + | "service_completed_successfully"; + restart?: boolean | string; + required?: boolean; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }; + }; + device_cgroup_rules?: string[]; + devices?: ( + | string + | { + source: string; + target?: string; + permissions?: string; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + } + )[]; + dns?: StringOrList; + dns_opt?: string[]; + dns_search?: StringOrList; + domainname?: string; + entrypoint?: Command; + env_file?: EnvFile; + label_file?: string | string[]; + environment?: ListOrDict; + expose?: (string | number)[]; + extends?: string | { service: string; file?: string }; + external_links?: string[]; + extra_hosts?: ExtraHosts; + gpus?: + | "all" + | Array<{ + capabilities?: string[]; + count?: string | number; + device_ids?: string[]; + driver?: string; + options?: ListOrDict; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }>; + group_add?: (string | number)[]; + healthcheck?: Healthcheck; + hostname?: string; + image?: string; + init?: boolean | string; + ipc?: string; + isolation?: string; + labels?: ListOrDict; + links?: string[]; + logging?: { + driver?: string; + options?: { [key: string]: string | number | null }; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }; + mac_address?: string; + mem_limit?: number | string; + mem_reservation?: string | number; + mem_swappiness?: number | string; + memswap_limit?: number | string; + network_mode?: string; + networks?: + | string[] + | { + [network: string]: { + aliases?: string[]; + ipv4_address?: string; + ipv6_address?: string; + link_local_ips?: string[]; + mac_address?: string; + driver_opts?: { [key: string]: string | number }; + priority?: number; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + } | null; + }; + oom_kill_disable?: boolean | string; + oom_score_adj?: string | number; + pid?: string | null; + pids_limit?: number | string; + platform?: string; + ports?: ( + | number + | string + | { + name?: string; + mode?: string; + host_ip?: string; + target?: number | string; + published?: string | number; + protocol?: string; + app_protocol?: string; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + } + )[]; + post_start?: ServiceHook[]; + pre_stop?: ServiceHook[]; + privileged?: boolean | string; + profiles?: string[]; + pull_policy?: "always" | "never" | "if_not_present" | "build" | "missing"; + read_only?: boolean | string; + restart?: string; + runtime?: string; + scale?: number | string; + security_opt?: string[]; + shm_size?: number | string; + secrets?: ServiceConfigOrSecret[]; + sysctls?: ListOrDict; + stdin_open?: boolean | string; + stop_grace_period?: string; + stop_signal?: string; + storage_opt?: object; + tmpfs?: StringOrList; + tty?: boolean | string; + ulimits?: Ulimits; + user?: string; + uts?: string; + userns_mode?: string; + volumes?: ( + | string + | { + type: string; + source?: string; + target?: string; + read_only?: boolean | string; + consistency?: string; + bind?: { + propagation?: string; + create_host_path?: boolean | string; + recursive?: "enabled" | "disabled" | "writable" | "readonly"; + selinux?: "z" | "Z"; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }; + volume?: { + nocopy?: boolean | string; + subpath?: string; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }; + tmpfs?: { + size?: number | string; + mode?: number | string; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + } + )[]; + volumes_from?: string[]; + working_dir?: string; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; } interface Healthcheck { - disable?: boolean | string; - interval?: string; - retries?: number | string; - test?: string | string[]; - timeout?: string; - start_period?: string; - start_interval?: string; - [key: `x-${string}`]: any; + disable?: boolean | string; + interval?: string; + retries?: number | string; + test?: string | string[]; + timeout?: string; + start_period?: string; + start_interval?: string; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; } interface Development { - watch?: Array<{ - path: string; - action: "rebuild" | "sync" | "restart" | "sync+restart" | "sync+exec"; - ignore?: string[]; - target?: string; - exec?: ServiceHook; - [key: `x-${string}`]: any; - }>; - [key: `x-${string}`]: any; + watch?: Array<{ + path: string; + action: "rebuild" | "sync" | "restart" | "sync+restart" | "sync+exec"; + ignore?: string[]; + target?: string; + exec?: ServiceHook; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }>; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; } interface Deployment { - mode?: string; - endpoint_mode?: string; - replicas?: number | string; - labels?: ListOrDict; - rollback_config?: { - parallelism?: number | string; - delay?: string; - failure_action?: string; - monitor?: string; - max_failure_ratio?: number | string; - order?: "start-first" | "stop-first"; - [key: `x-${string}`]: any; - }; - update_config?: { - parallelism?: number | string; - delay?: string; - failure_action?: string; - monitor?: string; - max_failure_ratio?: number | string; - order?: "start-first" | "stop-first"; - [key: `x-${string}`]: any; - }; - resources?: { - limits?: { - cpus?: number | string; - memory?: string; - pids?: number | string; - [key: `x-${string}`]: any; - }; - reservations?: { - cpus?: number | string; - memory?: string; - generic_resources?: Array<{ - discrete_resource_spec?: { - kind?: string; - value?: number | string; - [key: `x-${string}`]: any; - }; - [key: `x-${string}`]: any; - }>; - devices?: Array<{ - capabilities?: string[]; - count?: string | number; - device_ids?: string[]; - driver?: string; - options?: ListOrDict; - [key: `x-${string}`]: any; - }>; - [key: `x-${string}`]: any; - }; - [key: `x-${string}`]: any; - }; - restart_policy?: { - condition?: string; - delay?: string; - max_attempts?: number | string; - window?: string; - [key: `x-${string}`]: any; - }; - placement?: { - constraints?: string[]; - preferences?: Array<{ - spread?: string; - [key: `x-${string}`]: any; - }>; - max_replicas_per_node?: number | string; - [key: `x-${string}`]: any; - }; - [key: `x-${string}`]: any; + mode?: string; + endpoint_mode?: string; + replicas?: number | string; + labels?: ListOrDict; + rollback_config?: { + parallelism?: number | string; + delay?: string; + failure_action?: string; + monitor?: string; + max_failure_ratio?: number | string; + order?: "start-first" | "stop-first"; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }; + update_config?: { + parallelism?: number | string; + delay?: string; + failure_action?: string; + monitor?: string; + max_failure_ratio?: number | string; + order?: "start-first" | "stop-first"; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }; + resources?: { + limits?: { + cpus?: number | string; + memory?: string; + pids?: number | string; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }; + reservations?: { + cpus?: number | string; + memory?: string; + generic_resources?: Array<{ + discrete_resource_spec?: { + kind?: string; + value?: number | string; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }>; + devices?: Array<{ + capabilities?: string[]; + count?: string | number; + device_ids?: string[]; + driver?: string; + options?: ListOrDict; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }>; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }; + restart_policy?: { + condition?: string; + delay?: string; + max_attempts?: number | string; + window?: string; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }; + placement?: { + constraints?: string[]; + preferences?: Array<{ + spread?: string; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }>; + max_replicas_per_node?: number | string; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; } type Command = string | string[] | null; type EnvFile = - | string - | Array< - string | { path: string; format?: string; required?: boolean | string } - >; + | string + | Array< + string | { path: string; format?: string; required?: boolean | string } + >; type StringOrList = string | string[]; type ListOrDict = - | { [key: string]: string | number | boolean | null } - | string[]; + | { [key: string]: string | number | boolean | null } + | string[]; type ExtraHosts = { [host: string]: string | string[] } | string[]; interface BlkioLimit { - path: string; - rate: number | string; + path: string; + rate: number | string; } interface BlkioWeight { - path: string; - weight: number | string; + path: string; + weight: number | string; } type ServiceConfigOrSecret = - | string - | { - source: string; - target?: string; - uid?: string; - gid?: string; - mode?: number | string; - [key: `x-${string}`]: any; - }; + | string + | { + source: string; + target?: string; + uid?: string; + gid?: string; + mode?: number | string; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }; type Ulimits = { - [key: string]: - | number - | string - | { hard: number | string; soft: number | string }; + [key: string]: + | number + | string + | { hard: number | string; soft: number | string }; }; interface ServiceHook { - command?: Command; - user?: string; - privileged?: boolean | string; - working_dir?: string; - environment?: ListOrDict; - [key: `x-${string}`]: any; + command?: Command; + user?: string; + privileged?: boolean | string; + working_dir?: string; + environment?: ListOrDict; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; } interface Network { - name?: string; - driver?: string; - driver_opts?: { [key: string]: string | number }; - ipam?: { - driver?: string; - config?: Array<{ - subnet?: string; - ip_range?: string; - gateway?: string; - aux_addresses?: { [key: string]: string }; - [key: `x-${string}`]: any; - }>; - options?: { [key: string]: string }; - [key: `x-${string}`]: any; - }; - external?: boolean | string | { name?: string; [key: `x-${string}`]: any }; - internal?: boolean | string; - enable_ipv4?: boolean | string; - enable_ipv6?: boolean | string; - attachable?: boolean | string; - labels?: ListOrDict; - [key: `x-${string}`]: any; + name?: string; + driver?: string; + driver_opts?: { [key: string]: string | number }; + ipam?: { + driver?: string; + config?: Array<{ + subnet?: string; + ip_range?: string; + gateway?: string; + aux_addresses?: { [key: string]: string }; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }>; + options?: { [key: string]: string }; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + external?: boolean | string | { name?: string; [key: `x-${string}`]: any }; + internal?: boolean | string; + enable_ipv4?: boolean | string; + enable_ipv6?: boolean | string; + attachable?: boolean | string; + labels?: ListOrDict; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; } interface Volume { - name?: string; - driver?: string; - driver_opts?: { [key: string]: string | number }; - external?: boolean | string | { name?: string; [key: `x-${string}`]: any }; - labels?: ListOrDict; - [key: `x-${string}`]: any; + name?: string; + driver?: string; + driver_opts?: { [key: string]: string | number }; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + external?: boolean | string | { name?: string; [key: `x-${string}`]: any }; + labels?: ListOrDict; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; } interface Secret { - name?: string; - environment?: string; - file?: string; - external?: boolean | string | { name?: string; [key: string]: any }; - labels?: ListOrDict; - driver?: string; - driver_opts?: { [key: string]: string | number }; - template_driver?: string; - [key: `x-${string}`]: any; + name?: string; + environment?: string; + file?: string; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + external?: boolean | string | { name?: string; [key: string]: any }; + labels?: ListOrDict; + driver?: string; + driver_opts?: { [key: string]: string | number }; + template_driver?: string; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; } interface Config { - name?: string; - content?: string; - environment?: string; - file?: string; - external?: boolean | string | { name?: string; [key: string]: any }; - labels?: ListOrDict; - template_driver?: string; - [key: `x-${string}`]: any; + name?: string; + content?: string; + environment?: string; + file?: string; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + external?: boolean | string | { name?: string; [key: string]: any }; + labels?: ListOrDict; + template_driver?: string; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; } diff --git a/src/typings/docker.ts b/src/typings/docker.ts index 8f8844b..7e3b01f 100644 --- a/src/typings/docker.ts +++ b/src/typings/docker.ts @@ -1,41 +1,41 @@ -import { ContainerStats } from "dockerode"; -import Docker from "dockerode"; +import type { ContainerStats } from "dockerode"; +import type Docker from "dockerode"; interface DockerHost { - name: string; - hostAddress: string; - secure: boolean; - id: number; + name: string; + hostAddress: string; + secure: boolean; + id: number; } interface ContainerInfo { - id: string; - hostId: string; - name: string; - image: string; - status: string; - state: string; - cpuUsage: number; - memoryUsage: number; - stats: ContainerStats; - info: Docker.ContainerInfo; + id: string; + hostId: string; + name: string; + image: string; + status: string; + state: string; + cpuUsage: number; + memoryUsage: number; + stats?: ContainerStats; + info?: Docker.ContainerInfo; } interface HostStats { - hostName: string; - hostId: number; - dockerVersion: string; - apiVersion: string; - os: string; - architecture: string; - totalMemory: number; - totalCPU: number; - labels: string[]; - containers: number; - containersRunning: number; - containersStopped: number; - containersPaused: number; - images: number; + hostName: string; + hostId: number; + dockerVersion: string; + apiVersion: string; + os: string; + architecture: string; + totalMemory: number; + totalCPU: number; + labels: string[]; + containers: number; + containersRunning: number; + containersStopped: number; + containersPaused: number; + images: number; } export type { HostStats, ContainerInfo, DockerHost }; diff --git a/src/typings/dockerode.ts b/src/typings/dockerode.ts index a460433..e1268ad 100644 --- a/src/typings/dockerode.ts +++ b/src/typings/dockerode.ts @@ -1,162 +1,162 @@ interface DockerInfo { - ID: string; - Containers: number; - ContainersRunning: number; - ContainersPaused: number; - ContainersStopped: number; - Images: number; - Driver: string; - DriverStatus: [string, string][]; - DockerRootDir: string; - SystemStatus: [string, string][]; - Plugins: { - Volume: string[]; - Network: string[]; - Authorization: string[]; - Log: string[]; - }; - MemoryLimit: boolean; - SwapLimit: boolean; - KernelMemory: boolean; - CpuCfsPeriod: boolean; - CpuCfsQuota: boolean; - CPUShares: boolean; - CPUSet: boolean; - OomKillDisable: boolean; - IPv4Forwarding: boolean; - BridgeNfIptables: boolean; - BridgeNfIp6tables: boolean; - Debug: boolean; - NFd: number; - NGoroutines: number; - SystemTime: string; - LoggingDriver: string; - CgroupDriver: string; - NEventsListener: number; - KernelVersion: string; - OperatingSystem: string; - OSType: string; - Architecture: string; - NCPU: number; - MemTotal: number; - IndexServerAddress: string; - RegistryConfig: { - AllowNondistributableArtifactsCIDRs: string[]; - AllowNondistributableArtifactsHostnames: string[]; - InsecureRegistryCIDRs: string[]; - IndexConfigs: Record< - string, - { - Name: string; - Mirrors: string[]; - Secure: boolean; - Official: boolean; - } - >; - Mirrors: string[]; - }; - GenericResources: Array< - | { DiscreteResourceSpec: { Kind: string; Value: number } } - | { NamedResourceSpec: { Kind: string; Value: string } } - >; - HttpProxy: string; - HttpsProxy: string; - NoProxy: string; - Name: string; - Labels: string[]; - ExperimentalBuild: boolean; - ServerVersion: string; - ClusterStore: string; - ClusterAdvertise: string; - Runtimes: Record< - string, - { - path: string; - runtimeArgs?: string[]; - } - >; - DefaultRuntime: string; - Swarm: { - NodeID: string; - NodeAddr: string; - LocalNodeState: string; - ControlAvailable: boolean; - Error: string; - RemoteManagers: Array<{ - NodeID: string; - Addr: string; - }>; - Nodes: number; - Managers: number; - Cluster: { - ID: string; - Version: { - Index: number; - }; - CreatedAt: string; - UpdatedAt: string; - Spec: { - Name: string; - Labels: Record; - Orchestration: { - TaskHistoryRetentionLimit: number; - }; - Raft: { - SnapshotInterval: number; - KeepOldSnapshots: number; - LogEntriesForSlowFollowers: number; - ElectionTick: number; - HeartbeatTick: number; - }; - Dispatcher: { - HeartbeatPeriod: number; - }; - CAConfig: { - NodeCertExpiry: number; - ExternalCAs: Array<{ - Protocol: string; - URL: string; - Options: Record; - CACert: string; - }>; - SigningCACert: string; - SigningCAKey: string; - ForceRotate: number; - }; - EncryptionConfig: { - AutoLockManagers: boolean; - }; - TaskDefaults: { - LogDriver: { - Name: string; - Options: Record; - }; - }; - }; - TLSInfo: { - TrustRoot: string; - CertIssuerSubject: string; - CertIssuerPublicKey: string; - }; - RootRotationInProgress: boolean; - }; - }; - LiveRestoreEnabled: boolean; - Isolation: string; - InitBinary: string; - ContainerdCommit: { - ID: string; - Expected: string; - }; - RuncCommit: { - ID: string; - Expected: string; - }; - InitCommit: { - ID: string; - Expected: string; - }; - SecurityOptions: string[]; + ID: string; + Containers: number; + ContainersRunning: number; + ContainersPaused: number; + ContainersStopped: number; + Images: number; + Driver: string; + DriverStatus: [string, string][]; + DockerRootDir: string; + SystemStatus: [string, string][]; + Plugins: { + Volume: string[]; + Network: string[]; + Authorization: string[]; + Log: string[]; + }; + MemoryLimit: boolean; + SwapLimit: boolean; + KernelMemory: boolean; + CpuCfsPeriod: boolean; + CpuCfsQuota: boolean; + CPUShares: boolean; + CPUSet: boolean; + OomKillDisable: boolean; + IPv4Forwarding: boolean; + BridgeNfIptables: boolean; + BridgeNfIp6tables: boolean; + Debug: boolean; + NFd: number; + NGoroutines: number; + SystemTime: string; + LoggingDriver: string; + CgroupDriver: string; + NEventsListener: number; + KernelVersion: string; + OperatingSystem: string; + OSType: string; + Architecture: string; + NCPU: number; + MemTotal: number; + IndexServerAddress: string; + RegistryConfig: { + AllowNondistributableArtifactsCIDRs: string[]; + AllowNondistributableArtifactsHostnames: string[]; + InsecureRegistryCIDRs: string[]; + IndexConfigs: Record< + string, + { + Name: string; + Mirrors: string[]; + Secure: boolean; + Official: boolean; + } + >; + Mirrors: string[]; + }; + GenericResources: Array< + | { DiscreteResourceSpec: { Kind: string; Value: number } } + | { NamedResourceSpec: { Kind: string; Value: string } } + >; + HttpProxy: string; + HttpsProxy: string; + NoProxy: string; + Name: string; + Labels: string[]; + ExperimentalBuild: boolean; + ServerVersion: string; + ClusterStore: string; + ClusterAdvertise: string; + Runtimes: Record< + string, + { + path: string; + runtimeArgs?: string[]; + } + >; + DefaultRuntime: string; + Swarm: { + NodeID: string; + NodeAddr: string; + LocalNodeState: string; + ControlAvailable: boolean; + Error: string; + RemoteManagers: Array<{ + NodeID: string; + Addr: string; + }>; + Nodes: number; + Managers: number; + Cluster: { + ID: string; + Version: { + Index: number; + }; + CreatedAt: string; + UpdatedAt: string; + Spec: { + Name: string; + Labels: Record; + Orchestration: { + TaskHistoryRetentionLimit: number; + }; + Raft: { + SnapshotInterval: number; + KeepOldSnapshots: number; + LogEntriesForSlowFollowers: number; + ElectionTick: number; + HeartbeatTick: number; + }; + Dispatcher: { + HeartbeatPeriod: number; + }; + CAConfig: { + NodeCertExpiry: number; + ExternalCAs: Array<{ + Protocol: string; + URL: string; + Options: Record; + CACert: string; + }>; + SigningCACert: string; + SigningCAKey: string; + ForceRotate: number; + }; + EncryptionConfig: { + AutoLockManagers: boolean; + }; + TaskDefaults: { + LogDriver: { + Name: string; + Options: Record; + }; + }; + }; + TLSInfo: { + TrustRoot: string; + CertIssuerSubject: string; + CertIssuerPublicKey: string; + }; + RootRotationInProgress: boolean; + }; + }; + LiveRestoreEnabled: boolean; + Isolation: string; + InitBinary: string; + ContainerdCommit: { + ID: string; + Expected: string; + }; + RuncCommit: { + ID: string; + Expected: string; + }; + InitCommit: { + ID: string; + Expected: string; + }; + SecurityOptions: string[]; } export type { DockerInfo }; diff --git a/src/typings/elysiajs.ts b/src/typings/elysiajs.ts index 913ceea..a68bb8c 100644 --- a/src/typings/elysiajs.ts +++ b/src/typings/elysiajs.ts @@ -1,12 +1,12 @@ import type { StatusMap } from "elysia"; -import type { HTTPHeaders } from "elysia/dist/types"; import type { ElysiaCookie } from "elysia/dist/cookies"; +import type { HTTPHeaders } from "elysia/dist/types"; interface set { - headers: HTTPHeaders; - status?: number | keyof StatusMap; - redirect?: string; - cookie?: Record; + headers: HTTPHeaders; + status?: number | keyof StatusMap; + redirect?: string; + cookie?: Record; } -export { set }; +export type { set }; diff --git a/src/typings/misc.ts b/src/typings/misc.ts new file mode 100644 index 0000000..c8bdc46 --- /dev/null +++ b/src/typings/misc.ts @@ -0,0 +1,5 @@ +export type BackupInfo = { + filename: string; + date: Date; + backupNum: number; +}; diff --git a/src/typings/plugin.ts b/src/typings/plugin.ts index 6ca68bf..c5c3cc0 100644 --- a/src/typings/plugin.ts +++ b/src/typings/plugin.ts @@ -1,26 +1,26 @@ -import { ContainerInfo } from "~/typings/docker"; +import type { ContainerInfo } from "~/typings/docker"; interface Plugin { - name: string; + name: string; - // Container lifecycle hooks - onContainerStart?: (containerInfo: ContainerInfo) => void; - onContainerStop?: (containerInfo: ContainerInfo) => void; - onContainerExit?: (containerInfo: ContainerInfo) => void; - onContainerCreate?: (containerInfo: ContainerInfo) => void; - onContainerKill?: (ContainerInfo: ContainerInfo) => void; - handleContainerDie?: (ContainerInfo: ContainerInfo) => void; - onContainerDestroy?: (containerInfo: ContainerInfo) => void; - onContainerPause?: (containerInfo: ContainerInfo) => void; - onContainerUnpause?: (containerInfo: ContainerInfo) => void; - onContainerRestart?: (containerInfo: ContainerInfo) => void; - onContainerUpdate?: (containerInfo: ContainerInfo) => void; - onContainerRename?: (containerInfo: ContainerInfo) => void; - onContainerHealthStatus?: (containerInfo: ContainerInfo) => void; + // Container lifecycle hooks + onContainerStart?: (containerInfo: ContainerInfo) => void; + onContainerStop?: (containerInfo: ContainerInfo) => void; + onContainerExit?: (containerInfo: ContainerInfo) => void; + onContainerCreate?: (containerInfo: ContainerInfo) => void; + onContainerKill?: (ContainerInfo: ContainerInfo) => void; + handleContainerDie?: (ContainerInfo: ContainerInfo) => void; + onContainerDestroy?: (containerInfo: ContainerInfo) => void; + onContainerPause?: (containerInfo: ContainerInfo) => void; + onContainerUnpause?: (containerInfo: ContainerInfo) => void; + onContainerRestart?: (containerInfo: ContainerInfo) => void; + onContainerUpdate?: (containerInfo: ContainerInfo) => void; + onContainerRename?: (containerInfo: ContainerInfo) => void; + onContainerHealthStatus?: (containerInfo: ContainerInfo) => void; - // Host lifecycle hooks - onHostUnreachable?: (host: string, err: string) => void; - onHostReachableAgain?: (host: string) => void; + // Host lifecycle hooks + onHostUnreachable?: (host: string, err: string) => void; + onHostReachableAgain?: (host: string) => void; } export type { Plugin }; diff --git a/src/typings/websocket.ts b/src/typings/websocket.ts index 5635e3c..e7ed96d 100644 --- a/src/typings/websocket.ts +++ b/src/typings/websocket.ts @@ -1,15 +1,15 @@ interface stackSocketMessage { - message?: string; - type?: "stack-progress" | "stack-error" | "stack-status" | "stack-removed"; - data?: stackSocketData; + message?: string; + type?: "stack-progress" | "stack-error" | "stack-status" | "stack-removed"; + data?: stackSocketData; } interface stackSocketData { - stack_id: number; - message: string; - action?: string; - status?: string; - timestamp?: string; + stack_id: number; + message: string; + action?: string; + status?: string; + timestamp?: string; } -export { stackSocketMessage }; +export type { stackSocketMessage }; diff --git a/tsconfig.json b/tsconfig.json index 9c2d511..3a44e36 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,107 +1,107 @@ { - "compilerOptions": { - /* Visit https://aka.ms/tsconfig to read more about this file */ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ - /* Projects */ - // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ - // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ - // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ - // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ - // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ - // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - /* Language and Environment */ - "target": "ES2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, - // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - // "jsx": "preserve", /* Specify what JSX code is generated. */ - // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ - // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ - // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ - // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ - // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ - // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ - // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ - // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ - // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ - "outDir": "build/", - /* Modules */ - "module": "ES2022" /* Specify what module code is generated. */, - // "rootDir": "./", /* Specify the root folder within your source files. */ - "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, - // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - "paths": { - "~/*": ["./src/*"] - } /* Specify a set of entries that re-map imports to additional lookup locations. */, - // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ - "types": [ - "bun-types" - ] /* Specify type package names to be included without being referenced in a source file. */, - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ - "resolveJsonModule": true /* Enable importing .json files. */, - // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + /* Language and Environment */ + "target": "ES2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + "outDir": "build/", + /* Modules */ + "module": "ES2022" /* Specify what module code is generated. */, + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + "paths": { + "~/*": ["./src/*"] + } /* Specify a set of entries that re-map imports to additional lookup locations. */, + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + "types": [ + "bun-types" + ] /* Specify type package names to be included without being referenced in a source file. */, + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + "resolveJsonModule": true /* Enable importing .json files. */, + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ - /* JavaScript Support */ - // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ - // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ - // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ - /* Emit */ - // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - // "declarationMap": true, /* Create sourcemaps for d.ts files. */ - // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ - // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - // "outDir": "./", /* Specify an output folder for all emitted files. */ - // "removeComments": true, /* Disable emitting comments. */ - // "noEmit": true, /* Disable emitting files from a compilation. */ - // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ - // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ - // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ - // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ - // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ - // "newLine": "crlf", /* Set the newline character for emitting files. */ - // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ - // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ - // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ - // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ - // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ - // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ - /* Interop Constraints */ - // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ - // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, - // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, - /* Type Checking */ - "strict": true /* Enable all strict type-checking options. */, - // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ - // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ - // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ - // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ - // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ - // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ - // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ - // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ - // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ - // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ - // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ - // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ - // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ - // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - /* Completeness */ - // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ - } + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } } From a8e30550b5dcddad578a2f9f7664563b8a94ee42 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 16 Apr 2025 20:16:53 +0000 Subject: [PATCH 241/324] Update dependency graphs --- dependency-graph.mmd | 332 ++++----- dependency-graph.svg | 1541 ++++++++++++++++++++++-------------------- 2 files changed, 996 insertions(+), 877 deletions(-) diff --git a/dependency-graph.mmd b/dependency-graph.mmd index 1ba6254..bb9e74a 100644 --- a/dependency-graph.mmd +++ b/dependency-graph.mmd @@ -10,208 +10,222 @@ subgraph 0["src"] 1["index.ts"] subgraph 2["routes"] 3["live-stacks.ts"] -P["live-logs.ts"] -1D["api-config.ts"] -1F["docker-manager.ts"] -1G["docker-stats.ts"] -1H["docker-websocket.ts"] -1J["logs.ts"] -1K["stacks.ts"] -1N["utils.ts"] +S["live-logs.ts"] +1G["api-config.ts"] +1I["docker-manager.ts"] +1J["docker-stats.ts"] +1K["docker-websocket.ts"] +1M["logs.ts"] +1N["stacks.ts"] +1Q["utils.ts"] end subgraph 4["core"] subgraph 5["utils"] 6["logger.ts"] -M["helpers.ts"] -10["calculations.ts"] -15["change-me-checker.ts"] -17["package-json.ts"] -19["swagger-readme.ts"] -1E["response-handler.ts"] +Q["helpers.ts"] +14["calculations.ts"] +18["change-me-checker.ts"] +1A["package-json.ts"] +1C["swagger-readme.ts"] +1H["response-handler.ts"] end subgraph 8["database"] -9["index.ts"] -A["config.ts"] -B["database.ts"] -D["helper.ts"] -E["containerStats.ts"] -F["dockerHosts.ts"] -I["hostStats.ts"] -J["logs.ts"] -L["stacks.ts"] +9["_dbState.ts"] +A["index.ts"] +B["backup.ts"] +D["database.ts"] +F["helper.ts"] +I["config.ts"] +J["containerStats.ts"] +K["dockerHosts.ts"] +M["hostStats.ts"] +N["logs.ts"] +P["stacks.ts"] end -subgraph Q["docker"] -R["monitor.ts"] -X["client.ts"] -Y["scheduler.ts"] -Z["store-container-stats.ts"] -11["store-host-stats.ts"] +subgraph U["docker"] +V["monitor.ts"] +11["client.ts"] +12["scheduler.ts"] +13["store-container-stats.ts"] +15["store-host-stats.ts"] end -subgraph T["plugins"] -U["plugin-manager.ts"] -13["loader.ts"] +subgraph X["plugins"] +Y["plugin-manager.ts"] +17["loader.ts"] end -subgraph 1L["stacks"] -1M["controller.ts"] +subgraph 1O["stacks"] +1P["controller.ts"] end end subgraph G["typings"] -H["docker.ts"] -K["websocket.ts"] -N["database.ts"] -O["docker-compose.ts"] -W["plugin.ts"] -12["dockerode.ts"] -1C["elysiajs.ts"] +H["misc.ts"] +L["docker.ts"] +O["database.ts"] +R["docker-compose.ts"] +T["websocket.ts"] +10["plugin.ts"] +16["dockerode.ts"] +1F["elysiajs.ts"] end -subgraph 1A["middleware"] -1B["auth.ts"] +subgraph 1D["middleware"] +1E["auth.ts"] end end 7["path"] -C["bun:sqlite"] -S["bun"] -V["events"] -subgraph 14["fs"] -16["promises"] +subgraph C["fs"] +19["promises"] end -18["package.json"] -1I["stream"] +E["bun:sqlite"] +W["bun"] +Z["events"] +1B["package.json"] +1L["stream"] 1-->3 -1-->9 -1-->R -1-->Y -1-->13 -1-->6 +1-->A +1-->V +1-->12 1-->17 -1-->19 -1-->1B -1-->1D -1-->1F +1-->6 +1-->1A +1-->1C +1-->1E 1-->1G -1-->1H -1-->P +1-->1I 1-->1J 1-->1K +1-->S +1-->1M 1-->1N -1-->N +1-->1Q +1-->O 3-->6 -3-->K +3-->T 6-->9 -6-->P -6-->N +6-->A +6-->S +6-->O 6-->7 -9-->A -9-->E -9-->B -9-->F -9-->I -9-->J -9-->L A-->B +A-->I +A-->J A-->D +A-->K +A-->M +A-->N +A-->P +B-->9 +B-->D +B-->F +B-->6 +B-->H B-->C -D-->6 -E-->B -E-->D -F-->B -F-->D -F-->H -I-->B +D-->E +D-->C +F-->9 +F-->6 I-->D -I-->H -J-->B +I-->F J-->D -J-->K -L-->M -L-->B -L-->D -L-->N -L-->O -M-->6 -P-->6 -P-->N -R-->U -R-->9 -R-->X -R-->6 -R-->H -R-->H -R-->S -U-->6 -U-->H -U-->W -U-->V -W-->H -X-->6 -X-->H -Y-->9 -Y-->Z -Y-->11 +J-->F +K-->D +K-->F +K-->L +M-->D +M-->F +M-->L +N-->D +N-->F +N-->O +P-->Q +P-->D +P-->F +P-->O +P-->R +Q-->6 +S-->6 +S-->O +V-->Y +V-->A +V-->11 +V-->6 +V-->L +V-->W Y-->6 -Y-->N -Z-->6 -Z-->9 -Z-->X -Z-->10 -11-->9 -11-->X -11-->M +Y-->L +Y-->10 +Y-->Z +10-->L 11-->6 -11-->H -11-->12 -13-->15 +11-->L +12-->A +12-->13 +12-->15 +12-->6 +12-->O 13-->6 -13-->U +13-->A +13-->11 13-->14 -13-->7 +15-->A +15-->11 +15-->Q 15-->6 +15-->L 15-->16 17-->18 -1B-->9 -1B-->6 -1B-->N -1B-->1C -1D-->9 -1D-->U -1D-->6 -1D-->17 -1D-->1E -1D-->1B -1D-->N +17-->6 +17-->Y +17-->C +17-->7 +18-->6 +18-->19 +1A-->1B +1E-->A 1E-->6 -1E-->1C -1F-->9 -1F-->6 -1F-->1E -1F-->H -1G-->9 -1G-->X -1G-->10 -1G-->M +1E-->O +1E-->1F +1G-->A +1G-->B +1G-->Y 1G-->6 +1G-->1A +1G-->1H 1G-->1E -1G-->H -1G-->12 -1H-->9 -1H-->X -1H-->10 +1G-->O +1G-->C 1H-->6 -1H-->1E -1H-->1I -1J-->9 +1H-->1F +1I-->A +1I-->6 +1I-->1H +1I-->L +1J-->A +1J-->11 +1J-->14 +1J-->Q 1J-->6 -1K-->9 -1K-->1M +1J-->1H +1J-->L +1J-->16 +1K-->A +1K-->11 +1K-->14 1K-->6 -1K-->1E -1M-->M -1M-->9 +1K-->1H +1K-->1L +1M-->A 1M-->6 -1M-->3 -1M-->N -1M-->O -1M-->16 -1N-->17 -1N-->1E +1N-->A +1N-->1P +1N-->6 +1N-->1H +1P-->Q +1P-->A +1P-->6 +1P-->3 +1P-->O +1P-->R +1P-->19 +1Q-->1A +1Q-->1H diff --git a/dependency-graph.svg b/dependency-graph.svg index 34974a2..3aa239a 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -4,72 +4,72 @@ - - + + dependency-cruiser output - + cluster_fs - -fs + +fs cluster_src - -src + +src cluster_src/core - -core + +core cluster_src/core/database - -database + +database cluster_src/core/docker - -docker + +docker cluster_src/core/plugins - -plugins + +plugins cluster_src/core/stacks - -stacks + +stacks cluster_src/core/utils - -utils + +utils cluster_src/middleware - -middleware + +middleware cluster_src/routes - -routes + +routes cluster_src/typings - -typings + +typings bun - -bun + +bun @@ -77,8 +77,8 @@ bun:sqlite - -bun:sqlite + +bun:sqlite @@ -86,8 +86,8 @@ events - -events + +events @@ -95,8 +95,8 @@ fs - -fs + +fs @@ -104,8 +104,8 @@ fs/promises - -promises + +promises @@ -113,8 +113,8 @@ package.json - -package.json + +package.json @@ -122,1249 +122,1354 @@ path - -path + +path - + -src/core/database/config.ts - - -config.ts +src/core/database/_dbState.ts + + +_dbState.ts - + -src/core/database/database.ts - - -database.ts +src/core/database/backup.ts + + +backup.ts - + + +src/core/database/backup.ts->fs + + + + -src/core/database/config.ts->src/core/database/database.ts - - +src/core/database/backup.ts->src/core/database/_dbState.ts + + - + -src/core/database/helper.ts - - -helper.ts +src/core/database/database.ts + + +database.ts - + -src/core/database/config.ts->src/core/database/helper.ts - - - - - - - -src/core/database/database.ts->bun:sqlite - - +src/core/database/backup.ts->src/core/database/database.ts + + - - -src/core/utils/logger.ts - - -logger.ts + + +src/core/database/helper.ts + + +helper.ts - - -src/core/database/helper.ts->src/core/utils/logger.ts - - - - + + +src/core/database/backup.ts->src/core/database/helper.ts + + + + - - -src/core/database/containerStats.ts - - -containerStats.ts + + +src/core/utils/logger.ts + + +logger.ts - - -src/core/database/containerStats.ts->src/core/database/database.ts - - - - + -src/core/database/containerStats.ts->src/core/database/helper.ts - - - - +src/core/database/backup.ts->src/core/utils/logger.ts + + + + - - -src/core/database/dockerHosts.ts - - -dockerHosts.ts + + +src/typings/misc.ts + + +misc.ts - - -src/core/database/dockerHosts.ts->src/core/database/database.ts - - + + +src/core/database/backup.ts->src/typings/misc.ts + + - - -src/core/database/dockerHosts.ts->src/core/database/helper.ts - - - - + + +src/core/database/database.ts->bun:sqlite + + - - -src/typings/docker.ts - - -docker.ts - + + +src/core/database/database.ts->fs + + + + +src/core/database/helper.ts->src/core/database/_dbState.ts + + - - -src/core/database/dockerHosts.ts->src/typings/docker.ts - - + + +src/core/database/helper.ts->src/core/utils/logger.ts + + + + - + src/core/utils/logger.ts->path - - + + + + + +src/core/utils/logger.ts->src/core/database/_dbState.ts + + - + src/core/database/index.ts - - -index.ts + + +index.ts - + src/core/utils/logger.ts->src/core/database/index.ts - - - - + + + + - + src/typings/database.ts - - -database.ts + + +database.ts - + src/core/utils/logger.ts->src/typings/database.ts - - + + - + src/routes/live-logs.ts - - -live-logs.ts + + +live-logs.ts - + src/core/utils/logger.ts->src/routes/live-logs.ts - - - - + + + + - + + +src/core/database/config.ts + + +config.ts + + + + + +src/core/database/config.ts->src/core/database/database.ts + + + + + +src/core/database/config.ts->src/core/database/helper.ts + + + + + + +src/core/database/containerStats.ts + + +containerStats.ts + + + + + +src/core/database/containerStats.ts->src/core/database/database.ts + + + + + +src/core/database/containerStats.ts->src/core/database/helper.ts + + + + + + + +src/core/database/dockerHosts.ts + + +dockerHosts.ts + + + + + +src/core/database/dockerHosts.ts->src/core/database/database.ts + + + + + +src/core/database/dockerHosts.ts->src/core/database/helper.ts + + + + + + + +src/typings/docker.ts + + +docker.ts + + + + + +src/core/database/dockerHosts.ts->src/typings/docker.ts + + + + + src/core/database/hostStats.ts - - -hostStats.ts + + +hostStats.ts - + src/core/database/hostStats.ts->src/core/database/database.ts - - + + - + src/core/database/hostStats.ts->src/core/database/helper.ts - - - - + + + + - + src/core/database/hostStats.ts->src/typings/docker.ts - - + + - - -src/core/database/index.ts->src/core/database/config.ts - - - - + + +src/core/database/index.ts->src/core/database/backup.ts + + + + - + src/core/database/index.ts->src/core/database/database.ts - - + + + + + +src/core/database/index.ts->src/core/database/config.ts + + + + - + src/core/database/index.ts->src/core/database/containerStats.ts - - - - + + + + - + src/core/database/index.ts->src/core/database/dockerHosts.ts - - - - + + + + - + src/core/database/index.ts->src/core/database/hostStats.ts - - - - + + + + - + src/core/database/logs.ts - - -logs.ts + + +logs.ts - + src/core/database/index.ts->src/core/database/logs.ts - - - - + + + + - + src/core/database/stacks.ts - - -stacks.ts + + +stacks.ts - + src/core/database/index.ts->src/core/database/stacks.ts - - - - + + + + - + src/core/database/logs.ts->src/core/database/database.ts - - + + - + src/core/database/logs.ts->src/core/database/helper.ts - - - - + + + + - - -src/typings/websocket.ts - - -websocket.ts - - - - - -src/core/database/logs.ts->src/typings/websocket.ts - - + + +src/core/database/logs.ts->src/typings/database.ts + + - + src/core/database/stacks.ts->src/core/database/database.ts - - + + - + src/core/database/stacks.ts->src/core/database/helper.ts - - - - + + + + + + + +src/core/database/stacks.ts->src/typings/database.ts + + - + src/core/utils/helpers.ts - - -helpers.ts + + +helpers.ts - + src/core/database/stacks.ts->src/core/utils/helpers.ts - - - - - - - -src/core/database/stacks.ts->src/typings/database.ts - - + + + + - + src/typings/docker-compose.ts - - -docker-compose.ts + + +docker-compose.ts - + src/core/database/stacks.ts->src/typings/docker-compose.ts - - + + - + src/core/utils/helpers.ts->src/core/utils/logger.ts - - - - + + + + - + src/core/docker/client.ts - - -client.ts + + +client.ts - - -src/core/docker/client.ts->src/typings/docker.ts - - - - + src/core/docker/client.ts->src/core/utils/logger.ts - - + + + + + +src/core/docker/client.ts->src/typings/docker.ts + + - + src/core/docker/monitor.ts - - -monitor.ts + + +monitor.ts - + src/core/docker/monitor.ts->bun - - - - - -src/core/docker/monitor.ts->src/typings/docker.ts - - + + - + src/core/docker/monitor.ts->src/core/utils/logger.ts - - + + + + + +src/core/docker/monitor.ts->src/typings/docker.ts + + - + src/core/docker/monitor.ts->src/core/database/index.ts - - + + - + src/core/docker/monitor.ts->src/core/docker/client.ts - - + + - + src/core/plugins/plugin-manager.ts - - -plugin-manager.ts + + +plugin-manager.ts - + src/core/docker/monitor.ts->src/core/plugins/plugin-manager.ts - - + + - + src/core/plugins/plugin-manager.ts->events - - - - - -src/core/plugins/plugin-manager.ts->src/typings/docker.ts - - + + - + src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts - - + + + + + +src/core/plugins/plugin-manager.ts->src/typings/docker.ts + + - + src/typings/plugin.ts - - -plugin.ts + + +plugin.ts - + src/core/plugins/plugin-manager.ts->src/typings/plugin.ts - - + + - + src/core/docker/scheduler.ts - - -scheduler.ts + + +scheduler.ts - + src/core/docker/scheduler.ts->src/core/utils/logger.ts - - + + - + src/core/docker/scheduler.ts->src/core/database/index.ts - - + + - + src/core/docker/scheduler.ts->src/typings/database.ts - - + + - + src/core/docker/store-container-stats.ts - - -store-container-stats.ts + + +store-container-stats.ts - + src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts - - + + - + src/core/docker/store-host-stats.ts - - -store-host-stats.ts + + +store-host-stats.ts - + src/core/docker/scheduler.ts->src/core/docker/store-host-stats.ts - - + + - + src/core/docker/store-container-stats.ts->src/core/utils/logger.ts - - + + - + src/core/docker/store-container-stats.ts->src/core/database/index.ts - - + + - + src/core/docker/store-container-stats.ts->src/core/docker/client.ts - - + + - + src/core/utils/calculations.ts - - -calculations.ts + + +calculations.ts - + src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts - - - - - -src/core/docker/store-host-stats.ts->src/typings/docker.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/utils/logger.ts - - + + + + + +src/core/docker/store-host-stats.ts->src/typings/docker.ts + + - + src/core/docker/store-host-stats.ts->src/core/database/index.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/utils/helpers.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/docker/client.ts - - + + - + src/typings/dockerode.ts - - -dockerode.ts + + +dockerode.ts - + src/core/docker/store-host-stats.ts->src/typings/dockerode.ts - - + + - + src/core/plugins/loader.ts - - -loader.ts + + +loader.ts - + src/core/plugins/loader.ts->fs - - + + - + src/core/plugins/loader.ts->path - - + + - + src/core/plugins/loader.ts->src/core/utils/logger.ts - - + + - + src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts - - + + - + src/core/utils/change-me-checker.ts - - -change-me-checker.ts + + +change-me-checker.ts - + src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts - - + + - + src/core/utils/change-me-checker.ts->fs/promises - - + + - + src/core/utils/change-me-checker.ts->src/core/utils/logger.ts - - + + - + src/typings/plugin.ts->src/typings/docker.ts - - + + - + src/core/stacks/controller.ts - - -controller.ts + + +controller.ts - + src/core/stacks/controller.ts->fs/promises - - + + - + src/core/stacks/controller.ts->src/core/utils/logger.ts - - + + - + src/core/stacks/controller.ts->src/core/database/index.ts - - - - - -src/core/stacks/controller.ts->src/core/utils/helpers.ts - - + + - + src/core/stacks/controller.ts->src/typings/database.ts - - + + + + + +src/core/stacks/controller.ts->src/core/utils/helpers.ts + + - + src/core/stacks/controller.ts->src/typings/docker-compose.ts - - + + - + src/routes/live-stacks.ts - - -live-stacks.ts + + +live-stacks.ts - + src/core/stacks/controller.ts->src/routes/live-stacks.ts - - + + - + src/routes/live-stacks.ts->src/core/utils/logger.ts - - + + + + + +src/typings/websocket.ts + + +websocket.ts + + - + src/routes/live-stacks.ts->src/typings/websocket.ts - - + + - + src/routes/live-logs.ts->src/core/utils/logger.ts - - - - + + + + - + src/routes/live-logs.ts->src/typings/database.ts - - + + - + src/core/utils/package-json.ts - - -package-json.ts + + +package-json.ts - + src/core/utils/package-json.ts->package.json - - + + - + src/core/utils/response-handler.ts - - -response-handler.ts + + +response-handler.ts - + src/core/utils/response-handler.ts->src/core/utils/logger.ts - - + + - + src/typings/elysiajs.ts - - -elysiajs.ts + + +elysiajs.ts - + src/core/utils/response-handler.ts->src/typings/elysiajs.ts - - + + - + src/core/utils/swagger-readme.ts - - -swagger-readme.ts + + +swagger-readme.ts - + src/index.ts - - -index.ts + + +index.ts - + src/index.ts->src/core/utils/logger.ts - - + + - + src/index.ts->src/core/database/index.ts - - + + - + src/index.ts->src/typings/database.ts - - + + - + src/index.ts->src/core/docker/monitor.ts - - + + - + src/index.ts->src/core/docker/scheduler.ts - - + + - + src/index.ts->src/core/plugins/loader.ts - - + + - + src/index.ts->src/routes/live-stacks.ts - - + + - + src/index.ts->src/routes/live-logs.ts - - + + - + src/index.ts->src/core/utils/package-json.ts - - + + - + src/index.ts->src/core/utils/swagger-readme.ts - - + + - + src/middleware/auth.ts - - -auth.ts + + +auth.ts - + src/index.ts->src/middleware/auth.ts - - + + - + src/routes/api-config.ts - - -api-config.ts + + +api-config.ts - + src/index.ts->src/routes/api-config.ts - - + + - + src/routes/docker-manager.ts - - -docker-manager.ts + + +docker-manager.ts - + src/index.ts->src/routes/docker-manager.ts - - + + - + src/routes/docker-stats.ts - - -docker-stats.ts + + +docker-stats.ts - + src/index.ts->src/routes/docker-stats.ts - - + + - + src/routes/docker-websocket.ts - - -docker-websocket.ts + + +docker-websocket.ts - + src/index.ts->src/routes/docker-websocket.ts - - + + - + src/routes/logs.ts - - -logs.ts + + +logs.ts - + src/index.ts->src/routes/logs.ts - - + + - + src/routes/stacks.ts - - -stacks.ts + + +stacks.ts - + src/index.ts->src/routes/stacks.ts - - + + - + src/routes/utils.ts - - -utils.ts + + +utils.ts - + src/index.ts->src/routes/utils.ts - - + + - + src/middleware/auth.ts->src/core/utils/logger.ts - - + + - + src/middleware/auth.ts->src/core/database/index.ts - - + + - + src/middleware/auth.ts->src/typings/database.ts - - + + - + src/middleware/auth.ts->src/typings/elysiajs.ts - - + + + + + +src/routes/api-config.ts->fs + + + + + +src/routes/api-config.ts->src/core/database/backup.ts + + - + src/routes/api-config.ts->src/core/utils/logger.ts - - + + - + src/routes/api-config.ts->src/core/database/index.ts - - + + - + src/routes/api-config.ts->src/typings/database.ts - - + + - + src/routes/api-config.ts->src/core/plugins/plugin-manager.ts - - + + - + src/routes/api-config.ts->src/core/utils/package-json.ts - - + + - + src/routes/api-config.ts->src/core/utils/response-handler.ts - - + + - + src/routes/api-config.ts->src/middleware/auth.ts - - - - - -src/routes/docker-manager.ts->src/typings/docker.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/logger.ts - - + + + + + +src/routes/docker-manager.ts->src/typings/docker.ts + + - + src/routes/docker-manager.ts->src/core/database/index.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/response-handler.ts - - - - - -src/routes/docker-stats.ts->src/typings/docker.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/logger.ts - - + + + + + +src/routes/docker-stats.ts->src/typings/docker.ts + + - + src/routes/docker-stats.ts->src/core/database/index.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/helpers.ts - - + + - + src/routes/docker-stats.ts->src/core/docker/client.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-stats.ts->src/typings/dockerode.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/response-handler.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-websocket.ts->src/core/database/index.ts - - + + - + src/routes/docker-websocket.ts->src/core/docker/client.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/response-handler.ts - - + + - + stream - - -stream + + +stream - + src/routes/docker-websocket.ts->stream - - + + - + src/routes/logs.ts->src/core/utils/logger.ts - - + + - + src/routes/logs.ts->src/core/database/index.ts - - + + - + src/routes/stacks.ts->src/core/utils/logger.ts - - + + - + src/routes/stacks.ts->src/core/database/index.ts - - + + - + src/routes/stacks.ts->src/core/stacks/controller.ts - - + + - + src/routes/stacks.ts->src/core/utils/response-handler.ts - - + + - + src/routes/utils.ts->src/core/utils/package-json.ts - - + + - + src/routes/utils.ts->src/core/utils/response-handler.ts - - + + From 82b28c8fb90dbbf8fb072b6527fe3aa40e85cce0 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 16 Apr 2025 22:17:53 +0200 Subject: [PATCH 242/324] CI/CD: Fix Lint --- .github/workflows/pipeline.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 4a4f895..26ee364 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -25,6 +25,8 @@ jobs: - name: Lint run: | + bun install + bun clean bun biome check --formatter-enabled=true --linter-enabled=true --organize-imports-enabled=true --reporter=github --fix src - name: Commit and Push Changes @@ -41,8 +43,6 @@ jobs: - name: Run Unit-tests run: | - bun install - bun clean bun test --reporter=junit --reporter-outfile=./bun.xml - name: Run Docker Build From 10c7d5b2c61c2d8d6177176cd44d757c2a986e9f Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 16 Apr 2025 22:18:42 +0200 Subject: [PATCH 243/324] CI/CD: Fix permissions --- .github/workflows/pipeline.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 26ee364..8d2eb09 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -7,7 +7,7 @@ on: types: [published, prereleased] permissions: - contents: read + contents: write packages: write jobs: From 2c636b09a9416f4394b2b5bbc60ca8dbc561209b Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 16 Apr 2025 20:19:06 +0000 Subject: [PATCH 244/324] Linting --- src/core/stacks/controller.ts | 560 +++++++++++++++++----------------- 1 file changed, 280 insertions(+), 280 deletions(-) diff --git a/src/core/stacks/controller.ts b/src/core/stacks/controller.ts index 8416b35..b6f6ddd 100644 --- a/src/core/stacks/controller.ts +++ b/src/core/stacks/controller.ts @@ -9,339 +9,339 @@ import type { ComposeSpec, Stack } from "~/typings/docker-compose"; import { findObjectByKey } from "../utils/helpers"; const wrapProgressCallback = (progressCallback?: (log: string) => void) => { - return progressCallback - ? (chunk: Buffer, streamSource?: "stdout" | "stderr") => { - const log = chunk.toString(); - progressCallback(log); - } - : undefined; + return progressCallback + ? (chunk: Buffer, streamSource?: "stdout" | "stderr") => { + const log = chunk.toString(); + progressCallback(log); + } + : undefined; }; async function getStackName(stack_id: number): Promise { - logger.debug(`Fetching stack name for id ${stack_id}`); - const stacks = dbFunctions.getStacks(); - const stack = findObjectByKey(stacks, "id", stack_id); - if (!stack) { - throw new Error(`Stack with id ${stack_id} not found`); - } - return stack.name; + logger.debug(`Fetching stack name for id ${stack_id}`); + const stacks = dbFunctions.getStacks(); + const stack = findObjectByKey(stacks, "id", stack_id); + if (!stack) { + throw new Error(`Stack with id ${stack_id} not found`); + } + return stack.name; } async function runStackCommand( - stack_id: number, - command: ( - cwd: string, - progressCallback?: (log: string) => void - ) => Promise, - action: string + stack_id: number, + command: ( + cwd: string, + progressCallback?: (log: string) => void, + ) => Promise, + action: string, ): Promise { - try { - const stackName = await getStackName(stack_id); - const stackPath = await getStackPath({ - id: stack_id, - name: stackName, - } as Stack); + try { + const stackName = await getStackName(stack_id); + const stackPath = await getStackPath({ + id: stack_id, + name: stackName, + } as Stack); - const progressCallback = (log: string) => { - postToClient({ - type: "stack-progress", - data: { - stack_id, - action, - message: log.trim(), - timestamp: new Date().toISOString(), - }, - }); - }; + const progressCallback = (log: string) => { + postToClient({ + type: "stack-progress", + data: { + stack_id, + action, + message: log.trim(), + timestamp: new Date().toISOString(), + }, + }); + }; - return await command(stackPath, progressCallback); - } catch (error) { - postToClient({ - type: "stack-error", - data: { - stack_id, - action, - message: String(error), - timestamp: new Date().toISOString(), - }, - }); - throw new Error( - `Error while ${action} stack "${stack_id}": ${String(error)}` - ); - } + return await command(stackPath, progressCallback); + } catch (error) { + postToClient({ + type: "stack-error", + data: { + stack_id, + action, + message: String(error), + timestamp: new Date().toISOString(), + }, + }); + throw new Error( + `Error while ${action} stack "${stack_id}": ${String(error)}`, + ); + } } async function getStackPath(stack: Stack): Promise { - const stackName = stack.name.trim().replace(/\s+/g, "_"); - return `stacks/${stackName}`; + const stackName = stack.name.trim().replace(/\s+/g, "_"); + return `stacks/${stackName}`; } async function createStackYAML(compose_spec: Stack): Promise { - const yaml = YAML.stringify(compose_spec.compose_spec); - const stackPath = await getStackPath(compose_spec); - await Bun.write(`${stackPath}/docker-compose.yaml`, yaml, { - createPath: true, - }); + const yaml = YAML.stringify(compose_spec.compose_spec); + const stackPath = await getStackPath(compose_spec); + await Bun.write(`${stackPath}/docker-compose.yaml`, yaml, { + createPath: true, + }); } export async function deployStack( - stack: ComposeSpec, - name: string, - version: number, - source: string, - automatic_reboot_on_error: boolean, - isCustom: boolean, - image_updates: boolean, - stack_prefix?: string + stack: ComposeSpec, + name: string, + version: number, + source: string, + automatic_reboot_on_error: boolean, + isCustom: boolean, + image_updates: boolean, + stack_prefix?: string, ): Promise { - let stackId: number; + let stackId: number; - try { - logger.debug(`Deploying Stack: ${JSON.stringify(stack)}`); - const serviceCount = stack.services - ? Object.keys(stack.services).length - : 0; - const resolvedPrefix = stack_prefix ?? ""; + try { + logger.debug(`Deploying Stack: ${JSON.stringify(stack)}`); + const serviceCount = stack.services + ? Object.keys(stack.services).length + : 0; + const resolvedPrefix = stack_prefix ?? ""; - const stack_config: stacks_config = { - id: 0, - name, - version, - source, - stack_prefix: resolvedPrefix, - automatic_reboot_on_error, - container_count: serviceCount, - custom: isCustom, - image_updates, - }; + const stack_config: stacks_config = { + id: 0, + name, + version, + source, + stack_prefix: resolvedPrefix, + automatic_reboot_on_error, + container_count: serviceCount, + custom: isCustom, + image_updates, + }; - if (!name) { - throw new Error("Stack name needed"); - } + if (!name) { + throw new Error("Stack name needed"); + } - stackId = dbFunctions.addStack(stack_config) as number; - postToClient({ - type: "stack-status", - data: { - stack_id: stackId, - status: "pending", - message: "Creating stack configuration", - }, - }); + stackId = dbFunctions.addStack(stack_config) as number; + postToClient({ + type: "stack-status", + data: { + stack_id: stackId, + status: "pending", + message: "Creating stack configuration", + }, + }); - const stackYaml: Stack = { - id: stackId, - name, - source, - version, - compose_spec: stack, - }; + const stackYaml: Stack = { + id: stackId, + name, + source, + version, + compose_spec: stack, + }; - await createStackYAML(stackYaml); + await createStackYAML(stackYaml); - await runStackCommand( - stackId, - (cwd, progressCallback) => - DockerCompose.upAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "deploying" - ); + await runStackCommand( + stackId, + (cwd, progressCallback) => + DockerCompose.upAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "deploying", + ); - postToClient({ - type: "stack-status", - data: { - stack_id: stackId, - status: "deployed", - message: "Stack deployed successfully", - }, - }); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - postToClient({ - type: "stack-error", - data: { - stack_id: 0, - action: "deploying", - message: errorMsg, - timestamp: new Date().toISOString(), - }, - }); - throw new Error(errorMsg); - } + postToClient({ + type: "stack-status", + data: { + stack_id: stackId, + status: "deployed", + message: "Stack deployed successfully", + }, + }); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + postToClient({ + type: "stack-error", + data: { + stack_id: 0, + action: "deploying", + message: errorMsg, + timestamp: new Date().toISOString(), + }, + }); + throw new Error(errorMsg); + } } export async function stopStack(stack_id: number): Promise { - // Note the await to discard the result (convert to void) - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.downAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "stopping" - ); + // Note the await to discard the result (convert to void) + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.downAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "stopping", + ); } export async function startStack(stack_id: number): Promise { - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.upAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "starting" - ); + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.upAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "starting", + ); } export async function pullStackImages(stack_id: number): Promise { - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.pullAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "pulling-images" - ); + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.pullAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "pulling-images", + ); } export async function restartStack(stack_id: number): Promise { - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.restartAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "restarting" - ); + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.restartAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "restarting", + ); } export async function getStackStatus( - stack_id: number - //biome-ignore lint/suspicious/noExplicitAny: + stack_id: number, + //biome-ignore lint/suspicious/noExplicitAny: ): Promise> { - const status = await runStackCommand( - stack_id, - async (cwd) => { - const rawStatus = await DockerCompose.ps({ cwd }); - //biome-ignore lint/suspicious/noExplicitAny: - return rawStatus.data.services.reduce((acc: any, service: any) => { - acc[service.name] = service.state; - return acc; - }, {}); - }, - "status-check" - ); - return status; + const status = await runStackCommand( + stack_id, + async (cwd) => { + const rawStatus = await DockerCompose.ps({ cwd }); + //biome-ignore lint/suspicious/noExplicitAny: + return rawStatus.data.services.reduce((acc: any, service: any) => { + acc[service.name] = service.state; + return acc; + }, {}); + }, + "status-check", + ); + return status; } export async function removeStack(stack_id: number): Promise { - try { - await runStackCommand( - stack_id, - async (cwd, progressCallback) => { - await DockerCompose.down({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }); - }, - "removing" - ); + try { + await runStackCommand( + stack_id, + async (cwd, progressCallback) => { + await DockerCompose.down({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }); + }, + "removing", + ); - const stackName = await getStackName(stack_id); - const stackPath = await getStackPath({ - id: stack_id, - name: stackName, - } as Stack); + const stackName = await getStackName(stack_id); + const stackPath = await getStackPath({ + id: stack_id, + name: stackName, + } as Stack); - try { - await rm(stackPath, { recursive: true }); - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - postToClient({ - type: "stack-error", - data: { - stack_id, - action: "removing", - message: errorMsg, - timestamp: new Date().toISOString(), - }, - }); - throw new Error(errorMsg); - } + try { + await rm(stackPath, { recursive: true }); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + postToClient({ + type: "stack-error", + data: { + stack_id, + action: "removing", + message: errorMsg, + timestamp: new Date().toISOString(), + }, + }); + throw new Error(errorMsg); + } - dbFunctions.deleteStack(stack_id); - postToClient({ - type: "stack-removed", - data: { - stack_id, - message: "Stack removed successfully", - }, - }); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - postToClient({ - type: "stack-error", - data: { - stack_id, - action: "removing", - message: errorMsg, - timestamp: new Date().toISOString(), - }, - }); - throw new Error(errorMsg); - } + dbFunctions.deleteStack(stack_id); + postToClient({ + type: "stack-removed", + data: { + stack_id, + message: "Stack removed successfully", + }, + }); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + postToClient({ + type: "stack-error", + data: { + stack_id, + action: "removing", + message: errorMsg, + timestamp: new Date().toISOString(), + }, + }); + throw new Error(errorMsg); + } } //biome-ignore lint/suspicious/noExplicitAny: export async function getAllStacksStatus(): Promise> { - try { - const stacks = dbFunctions.getStacks(); + try { + const stacks = dbFunctions.getStacks(); - const statusResults = await Promise.all( - stacks.map(async (stack) => { - const status = await runStackCommand( - stack.id as number, - async (cwd) => { - const rawStatus = await DockerCompose.ps({ cwd }); - //biome-ignore lint/suspicious/noExplicitAny: - return rawStatus.data.services.reduce((acc: any, service: any) => { - acc[service.name] = service.state; - return acc; - }, {}); - }, - "status-check" - ); - return { stackId: stack.id, status }; - }) - ); + const statusResults = await Promise.all( + stacks.map(async (stack) => { + const status = await runStackCommand( + stack.id as number, + async (cwd) => { + const rawStatus = await DockerCompose.ps({ cwd }); + //biome-ignore lint/suspicious/noExplicitAny: + return rawStatus.data.services.reduce((acc: any, service: any) => { + acc[service.name] = service.state; + return acc; + }, {}); + }, + "status-check", + ); + return { stackId: stack.id, status }; + }), + ); - return statusResults.reduce( - (acc, { stackId, status }) => { - // Ensure stackId is used as a string if necessary, e.g. - acc[String(stackId)] = status; - return acc; - }, - //biome-ignore lint/suspicious/noExplicitAny: - {} as Record - ); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - throw new Error(errorMsg); - } + return statusResults.reduce( + (acc, { stackId, status }) => { + // Ensure stackId is used as a string if necessary, e.g. + acc[String(stackId)] = status; + return acc; + }, + //biome-ignore lint/suspicious/noExplicitAny: + {} as Record, + ); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + throw new Error(errorMsg); + } } From 2cd11e4fb0d45fa0e8df641e1597fecae240734b Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 16 Apr 2025 22:20:10 +0200 Subject: [PATCH 245/324] CI/CD: Fix command --- .github/workflows/pipeline.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 8d2eb09..dd91fb0 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -27,7 +27,7 @@ jobs: run: | bun install bun clean - bun biome check --formatter-enabled=true --linter-enabled=true --organize-imports-enabled=true --reporter=github --fix src + bun lint -- --reporter=github - name: Commit and Push Changes uses: EndBug/add-and-commit@v9 From 855959f6b2b0aaa67a3dddf67fd3247e98b9a9d1 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 16 Apr 2025 22:25:43 +0200 Subject: [PATCH 246/324] CI/CD: Fix junit reports --- .github/workflows/pipeline.yaml | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index dd91fb0..4ae1bae 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -27,15 +27,7 @@ jobs: run: | bun install bun clean - bun lint -- --reporter=github - - - name: Commit and Push Changes - uses: EndBug/add-and-commit@v9 - with: - add: "src" - message: "Linting" - committer_name: "GitHub Action" - committer_email: "action@github.com" + bun lint -- --reporter=junit > lint.xml - name: Start proxy run: | @@ -45,6 +37,12 @@ jobs: run: | bun test --reporter=junit --reporter-outfile=./bun.xml + - name: Publish Test Report + uses: mikepenz/action-junit-report@v5 + if: success() || failure() + with: + report_paths: "*.xml" + - name: Run Docker Build run: | bun build:docker From 488e27fd1d1d36591da6c840b94295fa61442362 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 16 Apr 2025 22:35:41 +0200 Subject: [PATCH 247/324] CI/CD: Fix linter --- .github/workflows/pipeline.yaml | 8 +++++--- .gitignore | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 4ae1bae..f0f3d61 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -8,7 +8,9 @@ on: permissions: contents: write - packages: write + checks: write + id-token: write + pull-requests: write jobs: test: @@ -27,7 +29,7 @@ jobs: run: | bun install bun clean - bun lint -- --reporter=junit > lint.xml + bun biome check --formatter-enabled=true --linter-enabled=true --organize-imports-enabled=true --fix src --reporter=junit > lint.xml - name: Start proxy run: | @@ -35,7 +37,7 @@ jobs: - name: Run Unit-tests run: | - bun test --reporter=junit --reporter-outfile=./bun.xml + bun test --reporter=junit --reporter-outfile=./unit-test.xml - name: Publish Test Report uses: mikepenz/action-junit-report@v5 diff --git a/.gitignore b/.gitignore index 527c7b3..941ca3d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ .test dependency-graph* build -data \ No newline at end of file +data +*.xml \ No newline at end of file From 2570cb881a7ea5b977a41e1bbae9c275eea402ce Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 16 Apr 2025 22:37:46 +0200 Subject: [PATCH 248/324] CI/CD: Logging to debug --- .github/workflows/pipeline.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index f0f3d61..f77350b 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -38,6 +38,7 @@ jobs: - name: Run Unit-tests run: | bun test --reporter=junit --reporter-outfile=./unit-test.xml + ls -lah - name: Publish Test Report uses: mikepenz/action-junit-report@v5 From fe46d26ee39ed17a9eccacfcb120bde3aa4e4f81 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 16 Apr 2025 22:44:21 +0200 Subject: [PATCH 249/324] CI/CD: This seems stupid --- .github/workflows/pipeline.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index f77350b..7f8f09d 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -31,6 +31,12 @@ jobs: bun clean bun biome check --formatter-enabled=true --linter-enabled=true --organize-imports-enabled=true --fix src --reporter=junit > lint.xml + - name: Publish Test Report + uses: mikepenz/action-junit-report@v5 + if: success() || failure() + with: + report_paths: "*.xml" + - name: Start proxy run: | docker compose -f docker/docker-compose.dev.yaml up -d From 7846fedb3bcd80c6c368cf99ea816bd2693d703b Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 16 Apr 2025 22:46:14 +0200 Subject: [PATCH 250/324] CI/CD: Maybe this? --- .github/workflows/pipeline.yaml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 7f8f09d..56e25f3 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -31,12 +31,6 @@ jobs: bun clean bun biome check --formatter-enabled=true --linter-enabled=true --organize-imports-enabled=true --fix src --reporter=junit > lint.xml - - name: Publish Test Report - uses: mikepenz/action-junit-report@v5 - if: success() || failure() - with: - report_paths: "*.xml" - - name: Start proxy run: | docker compose -f docker/docker-compose.dev.yaml up -d @@ -50,7 +44,7 @@ jobs: uses: mikepenz/action-junit-report@v5 if: success() || failure() with: - report_paths: "*.xml" + report_paths: "*/**/*.xml" - name: Run Docker Build run: | From e17d6b8fb9e7ed413afb50c57707127b6483cc24 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 16 Apr 2025 22:47:38 +0200 Subject: [PATCH 251/324] CI/CD: Yes? No? --- .github/workflows/pipeline.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 56e25f3..fbfecf3 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -29,7 +29,7 @@ jobs: run: | bun install bun clean - bun biome check --formatter-enabled=true --linter-enabled=true --organize-imports-enabled=true --fix src --reporter=junit > lint.xml + bun biome check --formatter-enabled=true --linter-enabled=true --organize-imports-enabled=true --fix src --reporter=junit > lint-test.xml - name: Start proxy run: | From 69cc950ace8c2026a30a8879210d149f1daec563 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 16 Apr 2025 22:51:06 +0200 Subject: [PATCH 252/324] CI/CD: This might be it --- .github/workflows/pipeline.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index fbfecf3..d3bb20b 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -45,6 +45,8 @@ jobs: if: success() || failure() with: report_paths: "*/**/*.xml" + include_passed: true + detailed_summary: true - name: Run Docker Build run: | From 18e41a24726d20180e5ae76b59cb8956b8e8654b Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 16 Apr 2025 22:54:19 +0200 Subject: [PATCH 253/324] CI/CD: Remove detailed summarrry --- .github/workflows/pipeline.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index d3bb20b..725692c 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -46,7 +46,6 @@ jobs: with: report_paths: "*/**/*.xml" include_passed: true - detailed_summary: true - name: Run Docker Build run: | From e9e37a1e724fe7e3f4220de9fc614af40a1a6ba0 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 16 Apr 2025 22:55:30 +0200 Subject: [PATCH 254/324] CI/CD: Fix path --- .github/workflows/pipeline.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 725692c..9ec0b46 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -44,7 +44,7 @@ jobs: uses: mikepenz/action-junit-report@v5 if: success() || failure() with: - report_paths: "*/**/*.xml" + report_paths: "**/*.xml" include_passed: true - name: Run Docker Build From 8fe2e717e58e4fd385b49e292ba6c230e06a7675 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 16 Apr 2025 23:14:16 +0200 Subject: [PATCH 255/324] CI/CD: Fix reports --- .github/workflows/pipeline.yaml | 18 ++++++++++++---- .gitignore | 3 ++- src/core/database/database.ts | 38 ++++++++++++++++----------------- 3 files changed, 35 insertions(+), 24 deletions(-) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 9ec0b46..8df21a0 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -25,11 +25,21 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Knip + run: | + bun knip -- --reporter markdown > knip.report.md + - name: Lint run: | bun install bun clean - bun biome check --formatter-enabled=true --linter-enabled=true --organize-imports-enabled=true --fix src --reporter=junit > lint-test.xml + bun biome check \ + --formatter-enabled=true \ + --linter-enabled=true \ + --organize-imports-enabled=true \ + --fix \ + --reporter=junit \ + src > lint-test.xml - name: Start proxy run: | @@ -42,10 +52,10 @@ jobs: - name: Publish Test Report uses: mikepenz/action-junit-report@v5 - if: success() || failure() with: - report_paths: "**/*.xml" - include_passed: true + report_paths: | + lint-test.xml + unit-test.xml - name: Run Docker Build run: | diff --git a/.gitignore b/.gitignore index 941ca3d..bb2174e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ dependency-graph* build data -*.xml \ No newline at end of file +*.xml +*.report.md \ No newline at end of file diff --git a/src/core/database/database.ts b/src/core/database/database.ts index db33ec9..d6a0be1 100644 --- a/src/core/database/database.ts +++ b/src/core/database/database.ts @@ -4,16 +4,16 @@ import { existsSync, mkdirSync } from "node:fs"; const dataFolder = "data"; if (!existsSync(dataFolder)) { - mkdirSync(dataFolder, { recursive: true }); + mkdirSync(dataFolder, { recursive: true }); } -export const databasePath = "data/dockstatapi.db"; +const databasePath = "data/dockstatapi.db"; export const db = new Database(databasePath, { strict: true }); db.exec("PRAGMA journal_mode = WAL;"); export function init() { - db.exec(` + db.exec(` CREATE TABLE IF NOT EXISTS backend_log_entries ( timestamp STRING NOT NULL, level TEXT NOT NULL, @@ -77,25 +77,25 @@ export function init() { ); `); - const configRow = db - .prepare("SELECT COUNT(*) AS count FROM config") - .get() as { count: number }; + const configRow = db + .prepare("SELECT COUNT(*) AS count FROM config") + .get() as { count: number }; - if (configRow.count === 0) { - db.prepare( - 'INSERT INTO config (keep_data_for, fetching_interval, api_key) VALUES (7, 5, "changeme")', - ).run(); - } + if (configRow.count === 0) { + db.prepare( + 'INSERT INTO config (keep_data_for, fetching_interval, api_key) VALUES (7, 5, "changeme")' + ).run(); + } - const hostRow = db - .prepare("SELECT COUNT(*) AS count FROM docker_hosts") - .get() as { count: number }; + const hostRow = db + .prepare("SELECT COUNT(*) AS count FROM docker_hosts") + .get() as { count: number }; - if (hostRow.count === 0) { - db.prepare( - "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)", - ).run("Localhost", "localhost:2375", false); - } + if (hostRow.count === 0) { + db.prepare( + "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)" + ).run("Localhost", "localhost:2375", false); + } } init(); From 7936bb2e9c914d2be97863bdc5f0193fd6611aae Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 16 Apr 2025 23:15:06 +0200 Subject: [PATCH 256/324] CI/CD: Fix readd package permission --- .github/workflows/pipeline.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 8df21a0..7eefb5c 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -11,6 +11,7 @@ permissions: checks: write id-token: write pull-requests: write + packages: write jobs: test: From 39777b551808b32b76cf7814eb8d13fd19340bc8 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 16 Apr 2025 23:19:42 +0200 Subject: [PATCH 257/324] CI//CD: Add knip report --- .github/workflows/pipeline.yaml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 7eefb5c..44c6a3a 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -26,10 +26,6 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Knip - run: | - bun knip -- --reporter markdown > knip.report.md - - name: Lint run: | bun install @@ -41,6 +37,11 @@ jobs: --fix \ --reporter=junit \ src > lint-test.xml + bun knip -- --reporter markdown > knip.report.md + + - name: Read Knip report + id: getknip + run: echo "::set-output name=content::$(cat knip.report.md)" - name: Start proxy run: | @@ -57,6 +58,7 @@ jobs: report_paths: | lint-test.xml unit-test.xml + summary: ${{ steps.getknip.outputs.content }} - name: Run Docker Build run: | From 08772861d0116ceb0b08fc7ab02fbe1b08164ddc Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 16 Apr 2025 23:20:36 +0200 Subject: [PATCH 258/324] CI/CD: Fix add include_passed --- .github/workflows/pipeline.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 44c6a3a..bdb1cea 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -55,6 +55,7 @@ jobs: - name: Publish Test Report uses: mikepenz/action-junit-report@v5 with: + include_passed: true report_paths: | lint-test.xml unit-test.xml From cc0bf5ea7e1970ce6fe3237d1089a54b7b97a613 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 16 Apr 2025 23:24:32 +0200 Subject: [PATCH 259/324] CI/CD: Change to GH output --- .github/workflows/pipeline.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index bdb1cea..fcb55d2 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -15,6 +15,8 @@ permissions: jobs: test: + outputs: + knip: ${{ steps.getknip.outputs.test }} name: Test Build on Push runs-on: ubuntu-latest steps: @@ -41,7 +43,7 @@ jobs: - name: Read Knip report id: getknip - run: echo "::set-output name=content::$(cat knip.report.md)" + run: echo "{content}={$(cat knip.report.md)}" >> $GITHUB_OUTPUT - name: Start proxy run: | From 00f2d80794159d6064cacf3315ee025c058d1dd9 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 17 Apr 2025 16:56:57 +0200 Subject: [PATCH 260/324] CI/CD: FIx Reporting --- .github/CODEOWNERS | 1 + .github/workflows/pipeline.yaml | 18 +++++++++-------- .gitignore | 4 ++-- src/core/database/database.ts | 36 ++++++++++++++++----------------- 4 files changed, 31 insertions(+), 28 deletions(-) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..7eeacf3 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +*.ts info@itsnik.de \ No newline at end of file diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index fcb55d2..2d15b2f 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -15,8 +15,6 @@ permissions: jobs: test: - outputs: - knip: ${{ steps.getknip.outputs.test }} name: Test Build on Push runs-on: ubuntu-latest steps: @@ -39,11 +37,17 @@ jobs: --fix \ --reporter=junit \ src > lint-test.xml - bun knip -- --reporter markdown > knip.report.md + bun knip --reporter markdown > Knip-Report.md - - name: Read Knip report - id: getknip - run: echo "{content}={$(cat knip.report.md)}" >> $GITHUB_OUTPUT + - name: Create or update PR comment with Knip report + uses: peter-evans/create-or-update-comment@v3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + repository: ${{ github.repository }} + issue-number: ${{ github.event.pull_request.number }} + body-file: Knip-Report.md + comment-id: knip-report + edit-mode: replace - name: Start proxy run: | @@ -59,9 +63,7 @@ jobs: with: include_passed: true report_paths: | - lint-test.xml unit-test.xml - summary: ${{ steps.getknip.outputs.content }} - name: Run Docker Build run: | diff --git a/.gitignore b/.gitignore index bb2174e..569df45 100644 --- a/.gitignore +++ b/.gitignore @@ -2,8 +2,8 @@ /stacks /node_modules .test -dependency-graph* build data *.xml -*.report.md \ No newline at end of file +*.report.md +dependency-* \ No newline at end of file diff --git a/src/core/database/database.ts b/src/core/database/database.ts index d6a0be1..a8d4c13 100644 --- a/src/core/database/database.ts +++ b/src/core/database/database.ts @@ -4,7 +4,7 @@ import { existsSync, mkdirSync } from "node:fs"; const dataFolder = "data"; if (!existsSync(dataFolder)) { - mkdirSync(dataFolder, { recursive: true }); + mkdirSync(dataFolder, { recursive: true }); } const databasePath = "data/dockstatapi.db"; @@ -13,7 +13,7 @@ export const db = new Database(databasePath, { strict: true }); db.exec("PRAGMA journal_mode = WAL;"); export function init() { - db.exec(` + db.exec(` CREATE TABLE IF NOT EXISTS backend_log_entries ( timestamp STRING NOT NULL, level TEXT NOT NULL, @@ -77,25 +77,25 @@ export function init() { ); `); - const configRow = db - .prepare("SELECT COUNT(*) AS count FROM config") - .get() as { count: number }; + const configRow = db + .prepare("SELECT COUNT(*) AS count FROM config") + .get() as { count: number }; - if (configRow.count === 0) { - db.prepare( - 'INSERT INTO config (keep_data_for, fetching_interval, api_key) VALUES (7, 5, "changeme")' - ).run(); - } + if (configRow.count === 0) { + db.prepare( + 'INSERT INTO config (keep_data_for, fetching_interval, api_key) VALUES (7, 5, "changeme")', + ).run(); + } - const hostRow = db - .prepare("SELECT COUNT(*) AS count FROM docker_hosts") - .get() as { count: number }; + const hostRow = db + .prepare("SELECT COUNT(*) AS count FROM docker_hosts") + .get() as { count: number }; - if (hostRow.count === 0) { - db.prepare( - "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)" - ).run("Localhost", "localhost:2375", false); - } + if (hostRow.count === 0) { + db.prepare( + "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)", + ).run("Localhost", "localhost:2375", false); + } } init(); From deb505f117b399a445fa46efe2c6e9e1226dd1bd Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 17 Apr 2025 17:02:30 +0200 Subject: [PATCH 261/324] CI/CD: Fix pathing --- .github/workflows/pipeline.yaml | 10 +++++++++- .gitignore | 4 ++-- knip.report.md | Bin 0 -> 36 bytes 3 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 knip.report.md diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 2d15b2f..4d179bb 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -39,6 +39,14 @@ jobs: src > lint-test.xml bun knip --reporter markdown > Knip-Report.md + - name: Commit and Push Changes + uses: EndBug/add-and-commit@v9 + with: + add: "src/" + message: "Update dependency graphs" + committer_name: "GitHub Action" + committer_email: "action@github.com" + - name: Create or update PR comment with Knip report uses: peter-evans/create-or-update-comment@v3 with: @@ -46,7 +54,7 @@ jobs: repository: ${{ github.repository }} issue-number: ${{ github.event.pull_request.number }} body-file: Knip-Report.md - comment-id: knip-report + comment-id: knip-report.md edit-mode: replace - name: Start proxy diff --git a/.gitignore b/.gitignore index 569df45..1c7d1e1 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,5 @@ build data *.xml -*.report.md -dependency-* \ No newline at end of file +dependency-* +Knip-Report.md \ No newline at end of file diff --git a/knip.report.md b/knip.report.md new file mode 100644 index 0000000000000000000000000000000000000000..883973e8cd66b2063469f101056c1e31272ec606 GIT binary patch literal 36 jcmezWPnki1!J8qEA(Np1$SPt;1=9IIx`ct3feVZQqx=TF literal 0 HcmV?d00001 From 5cbe174829079015740c5fa57afbdd4f1ecceca6 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 25 Apr 2025 09:09:38 +0200 Subject: [PATCH 262/324] CI/CD: Update pipeline.yaml --- .github/workflows/pipeline.yaml | 34 +++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 4d179bb..70be649 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -126,3 +126,37 @@ jobs: push: true platforms: linux/amd64,linux/arm64 tags: ghcr.io/its4nik/dockstatapi:${{ env.IMAGE_TAG }} + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@0.28.0 + with: + image-ref: 'ghcr.io/its4nik/dockstatapi:${{ env.IMAGE_TAG }}' + format: 'sarif' + output: 'trivy-results.sarif' + exit-code: '1' + ignore-unfixed: true + vuln-type: 'os,library' + severity: "MEDIUM,HIGH,CRITICAL" + + - name: Scan image in a private registry + uses: aquasecurity/trivy-action@0.28.0 + with: + image-ref: "ghcr.io/its4nik/dockstatapi:${{ env.IMAGE_TAG }}" + scan-type: image + format: 'github' + output: 'dependency-results.sbom.json' + github-pat: ${{ secrets.GITHUB_TOKEN }} + severity: "MEDIUM,HIGH,CRITICAL" + scanners: "vuln" + + - name: Upload trivy report as a Github artifact + uses: actions/upload-artifact@v4 + with: + name: trivy-sbom-report + path: '${{ github.workspace }}/dependency-results.sbom.json' + retention-days: 20 + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: 'trivy-results.sarif' From 718ddd4a66f5d64e380e01cbaf186fbe23358614 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 25 Apr 2025 09:16:42 +0200 Subject: [PATCH 263/324] CI/CD: Update pipeline.yaml --- .github/workflows/pipeline.yaml | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 70be649..8897c87 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -30,33 +30,17 @@ jobs: run: | bun install bun clean - bun biome check \ - --formatter-enabled=true \ - --linter-enabled=true \ - --organize-imports-enabled=true \ - --fix \ - --reporter=junit \ - src > lint-test.xml - bun knip --reporter markdown > Knip-Report.md + bun biome ci + bun knip --reporter markdown > .github/Knip-Report.md - name: Commit and Push Changes uses: EndBug/add-and-commit@v9 with: - add: "src/" - message: "Update dependency graphs" + add: '["src/",".github/*.md"]' + message: "Update dependency graphs and upload Knip-report.md" committer_name: "GitHub Action" committer_email: "action@github.com" - - name: Create or update PR comment with Knip report - uses: peter-evans/create-or-update-comment@v3 - with: - token: ${{ secrets.GITHUB_TOKEN }} - repository: ${{ github.repository }} - issue-number: ${{ github.event.pull_request.number }} - body-file: Knip-Report.md - comment-id: knip-report.md - edit-mode: replace - - name: Start proxy run: | docker compose -f docker/docker-compose.dev.yaml up -d From 252eaae49864bdb21ebf42bb6ef8c0f0d21772f3 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 25 Apr 2025 09:20:47 +0200 Subject: [PATCH 264/324] CI/CD: pipeline.yaml --- .github/workflows/pipeline.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 8897c87..efa9253 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -116,12 +116,17 @@ jobs: with: image-ref: 'ghcr.io/its4nik/dockstatapi:${{ env.IMAGE_TAG }}' format: 'sarif' - output: 'trivy-results.sarif' + output: 'trivy-results-${{ env.IMAGE_TAG }}.sarif' exit-code: '1' ignore-unfixed: true vuln-type: 'os,library' severity: "MEDIUM,HIGH,CRITICAL" + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: 'trivy-results-${{ env.IMAGE_TAG }}.sarif' + - name: Scan image in a private registry uses: aquasecurity/trivy-action@0.28.0 with: @@ -139,8 +144,3 @@ jobs: name: trivy-sbom-report path: '${{ github.workspace }}/dependency-results.sbom.json' retention-days: 20 - - - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@v3 - with: - sarif_file: 'trivy-results.sarif' From 5ab15c7e42f6732a0fd423f9464b9c6283e9de57 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 25 Apr 2025 09:23:25 +0200 Subject: [PATCH 265/324] CI/CD: Update pipeline.yaml --- .github/workflows/pipeline.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index efa9253..7bd5457 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -12,6 +12,7 @@ permissions: id-token: write pull-requests: write packages: write + security-events: write jobs: test: @@ -127,7 +128,7 @@ jobs: with: sarif_file: 'trivy-results-${{ env.IMAGE_TAG }}.sarif' - - name: Scan image in a private registry + - name: Scan image dependencies uses: aquasecurity/trivy-action@0.28.0 with: image-ref: "ghcr.io/its4nik/dockstatapi:${{ env.IMAGE_TAG }}" From 022883268347d2e1b23891ef0994fb17aaeb9400 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 25 Apr 2025 09:29:43 +0200 Subject: [PATCH 266/324] Update pipeline.yaml --- .github/workflows/pipeline.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 7bd5457..432f7de 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -32,7 +32,7 @@ jobs: bun install bun clean bun biome ci - bun knip --reporter markdown > .github/Knip-Report.md + bun knip --reporter markdown -- > .github/Knip-Report.md - name: Commit and Push Changes uses: EndBug/add-and-commit@v9 From 8f4b05c2ba6ad1d2cd5f1e1bcd08464815094bb7 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 25 Apr 2025 09:36:07 +0200 Subject: [PATCH 267/324] CI/CD: Update pipeline.yaml --- .github/workflows/pipeline.yaml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 432f7de..97817ee 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -32,12 +32,15 @@ jobs: bun install bun clean bun biome ci - bun knip --reporter markdown -- > .github/Knip-Report.md + bun knip --reporter markdown > .github/Knip-Report.md + + - name: "Debug: list .github" + run: ls -l .github - name: Commit and Push Changes uses: EndBug/add-and-commit@v9 with: - add: '["src/",".github/*.md"]' + add: '["src/",".github/Knip-Report.md"]' message: "Update dependency graphs and upload Knip-report.md" committer_name: "GitHub Action" committer_email: "action@github.com" From b343c3c108fc0572aa313705fac579e5ff1f8ad0 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 25 Apr 2025 09:39:40 +0200 Subject: [PATCH 268/324] CI/CD: Update pipeline.yaml --- .github/workflows/pipeline.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 97817ee..94c570d 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -34,12 +34,10 @@ jobs: bun biome ci bun knip --reporter markdown > .github/Knip-Report.md - - name: "Debug: list .github" - run: ls -l .github - - name: Commit and Push Changes uses: EndBug/add-and-commit@v9 with: + commit: "-f" add: '["src/",".github/Knip-Report.md"]' message: "Update dependency graphs and upload Knip-report.md" committer_name: "GitHub Action" From 6c2b5b03dbd0977552ff7758f398dfde1bc037c3 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 25 Apr 2025 09:41:15 +0200 Subject: [PATCH 269/324] Update pipeline.yaml --- .github/workflows/pipeline.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 94c570d..9804f5c 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -32,13 +32,13 @@ jobs: bun install bun clean bun biome ci - bun knip --reporter markdown > .github/Knip-Report.md + bun knip --reporter markdown > Knip-Report.md - name: Commit and Push Changes uses: EndBug/add-and-commit@v9 with: commit: "-f" - add: '["src/",".github/Knip-Report.md"]' + add: '["src/","Knip-Report.md"]' message: "Update dependency graphs and upload Knip-report.md" committer_name: "GitHub Action" committer_email: "action@github.com" From 2ab6005570b58261f452b0263a7945b8a3bcb548 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 25 Apr 2025 09:47:21 +0200 Subject: [PATCH 270/324] CI/CD: Update pipeline.yaml --- .github/workflows/pipeline.yaml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 9804f5c..46c8c55 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -13,7 +13,8 @@ permissions: pull-requests: write packages: write security-events: write - + issues: write + jobs: test: name: Test Build on Push @@ -32,7 +33,9 @@ jobs: bun install bun clean bun biome ci - bun knip --reporter markdown > Knip-Report.md + + - name: Post the knip results + uses: codex-/knip-reporter@v2 - name: Commit and Push Changes uses: EndBug/add-and-commit@v9 From 3755750ab46c9cd5a3fa58e8c0e16fcf4d1932d5 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 25 Apr 2025 09:49:20 +0200 Subject: [PATCH 271/324] CI/CD: Update pipeline.yaml --- .github/workflows/pipeline.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 46c8c55..325e021 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -35,14 +35,15 @@ jobs: bun biome ci - name: Post the knip results + if: ${{ github.event_name == 'pull_request' }} uses: codex-/knip-reporter@v2 - name: Commit and Push Changes uses: EndBug/add-and-commit@v9 with: commit: "-f" - add: '["src/","Knip-Report.md"]' - message: "Update dependency graphs and upload Knip-report.md" + add: "src/" + message: "Update dependency graphs" committer_name: "GitHub Action" committer_email: "action@github.com" From 5ae009e91826f356dc21fecb32b155cff6ddee3d Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 29 Apr 2025 12:08:07 +0200 Subject: [PATCH 272/324] Create ci.yml --- .github/workflows/ci.yml | 77 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b6cae8d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,77 @@ +name: Continuous Integration + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + checks: write + security-events: write + +jobs: + lint-test: + name: Lint and Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Run linter + run: bun run biome ci + + - name: Run unit tests + run: bun test --reporter=junit --reporter-outfile=./unit-test.xml + + - name: Publish Test Report + uses: mikepenz/action-junit-report@v5 + with: + report_paths: 'unit-test.xml' + + build-scan: + name: Build and Security Scan + runs-on: ubuntu-latest + needs: lint-test + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: docker/Dockerfile + tags: dockstatapi:ci-${{ github.sha }} + load: true + + - name: Start and test container + run: | + docker run --name test-container -d dockstatapi:ci-${{ github.sha }} + sleep 10 + docker ps | grep test-container + docker logs test-container + docker stop test-container + + - name: Trivy vulnerability scan + uses: aquasecurity/trivy-action@0.28.0 + with: + image-ref: 'dockstatapi:ci-${{ github.sha }}' + format: 'sarif' + output: 'trivy-results.sarif' + severity: 'HIGH,CRITICAL' + + - name: Upload security results + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: 'trivy-results.sarif' From f90dc15ffea705cf1a21ebe183a645d4ac20c6dc Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 29 Apr 2025 12:08:49 +0200 Subject: [PATCH 273/324] Create cd.yml --- .github/workflows/cd.yml | 64 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 .github/workflows/cd.yml diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 0000000..15f58c4 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,64 @@ +name: Continuous Delivery + +on: + release: + types: [published, prereleased] + +permissions: + contents: read + packages: write + +jobs: + publish: + name: Publish Container Image + runs-on: ubuntu-latest + environment: production + steps: + - uses: actions/checkout@v4 + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Determine tags + id: tags + uses: docker/metadata-action@v5 + with: + images: ghcr.io/its4nik/dockstatapi + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=sha + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + file: docker/Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.tags.outputs.tags }} + + sbom: + name: Generate SBOM + runs-on: ubuntu-latest + needs: publish + steps: + - name: Generate SBOM + uses: aquasecurity/trivy-action@0.28.0 + with: + image-ref: ghcr.io/its4nik/dockstatapi:${{ github.event.release.tag_name }} + format: spdx-json + output: sbom.json + + - name: Upload SBOM + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: sbom.json From d95032f7a617399f00c2f4f77f0374feba339216 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 29 Apr 2025 12:09:29 +0200 Subject: [PATCH 274/324] Delete .github/workflows/pipeline.yaml --- .github/workflows/pipeline.yaml | 152 -------------------------------- 1 file changed, 152 deletions(-) delete mode 100644 .github/workflows/pipeline.yaml diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml deleted file mode 100644 index 325e021..0000000 --- a/.github/workflows/pipeline.yaml +++ /dev/null @@ -1,152 +0,0 @@ -name: Docker Build and Test Workflow - -on: - push: - branches: ["**"] - release: - types: [published, prereleased] - -permissions: - contents: write - checks: write - id-token: write - pull-requests: write - packages: write - security-events: write - issues: write - -jobs: - test: - name: Test Build on Push - runs-on: ubuntu-latest - steps: - - uses: oven-sh/setup-bun@v2 - name: Setup Bun - with: - bun-version: latest - - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Lint - run: | - bun install - bun clean - bun biome ci - - - name: Post the knip results - if: ${{ github.event_name == 'pull_request' }} - uses: codex-/knip-reporter@v2 - - - name: Commit and Push Changes - uses: EndBug/add-and-commit@v9 - with: - commit: "-f" - add: "src/" - message: "Update dependency graphs" - committer_name: "GitHub Action" - committer_email: "action@github.com" - - - name: Start proxy - run: | - docker compose -f docker/docker-compose.dev.yaml up -d - - - name: Run Unit-tests - run: | - bun test --reporter=junit --reporter-outfile=./unit-test.xml - ls -lah - - - name: Publish Test Report - uses: mikepenz/action-junit-report@v5 - with: - include_passed: true - report_paths: | - unit-test.xml - - - name: Run Docker Build - run: | - bun build:docker - - - name: Start Docker container and check uptime - run: | - docker run --name dockstatapi --rm -d dockstatapi:local - sleep 30 - if docker ps --filter "name=dockstatapi" --filter "status=running" | grep dockstatapi; then - docker kill dockstatapi - exit 0 - else - exit 1 - fi - - release: - name: Build and Push Docker Image on Release - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Determine image tag - id: tag - run: | - TAG=${GITHUB_REF##*/} - if [ "${{ github.event.release.prerelease }}" = "true" ]; then - TAG="${TAG}-rc" - fi - echo "IMAGE_TAG=$TAG" >> $GITHUB_ENV - echo "Using tag: $TAG" - - - name: Build and push Docker image - uses: docker/build-push-action@v6 - with: - context: . - file: docker/Dockerfile - push: true - platforms: linux/amd64,linux/arm64 - tags: ghcr.io/its4nik/dockstatapi:${{ env.IMAGE_TAG }} - - - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@0.28.0 - with: - image-ref: 'ghcr.io/its4nik/dockstatapi:${{ env.IMAGE_TAG }}' - format: 'sarif' - output: 'trivy-results-${{ env.IMAGE_TAG }}.sarif' - exit-code: '1' - ignore-unfixed: true - vuln-type: 'os,library' - severity: "MEDIUM,HIGH,CRITICAL" - - - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@v3 - with: - sarif_file: 'trivy-results-${{ env.IMAGE_TAG }}.sarif' - - - name: Scan image dependencies - uses: aquasecurity/trivy-action@0.28.0 - with: - image-ref: "ghcr.io/its4nik/dockstatapi:${{ env.IMAGE_TAG }}" - scan-type: image - format: 'github' - output: 'dependency-results.sbom.json' - github-pat: ${{ secrets.GITHUB_TOKEN }} - severity: "MEDIUM,HIGH,CRITICAL" - scanners: "vuln" - - - name: Upload trivy report as a Github artifact - uses: actions/upload-artifact@v4 - with: - name: trivy-sbom-report - path: '${{ github.workspace }}/dependency-results.sbom.json' - retention-days: 20 From 8a9e4f20cf5c1c8853548f30666f1105c68adaf1 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 29 Apr 2025 12:15:21 +0200 Subject: [PATCH 275/324] Update ci.yml --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b6cae8d..36bb85a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: Continuous Integration on: push: - branches: [main] + branches: ["**"] pull_request: - branches: [main] + branches: ["**"] permissions: contents: read From 5934b6c31bcbd761a1117a7ae9e27b05375cad1b Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 29 Apr 2025 13:25:29 +0200 Subject: [PATCH 276/324] Update ci.yml --- .github/workflows/ci.yml | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 36bb85a..501b994 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,17 +6,18 @@ on: pull_request: branches: ["**"] -permissions: - contents: read - checks: write - security-events: write - jobs: lint-test: name: Lint and Test runs-on: ubuntu-latest + permissions: + contents: write + checks: write + security-events: write steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Setup Bun uses: oven-sh/setup-bun@v2 @@ -29,6 +30,14 @@ jobs: - name: Run linter run: bun run biome ci + - name: Add linted files + run: git add src/ + + - name: Check for changes + id: check-changes + run: | + git diff --cached --quiet || echo "changes_detected=true" >> $GITHUB_OUTPUT + - name: Run unit tests run: bun test --reporter=junit --reporter-outfile=./unit-test.xml @@ -37,10 +46,24 @@ jobs: with: report_paths: 'unit-test.xml' + - name: Commit and push lint changes + if: | + steps.check-changes.outputs.changes_detected == 'true' && + github.event_name == 'push' + run: | + git config --global user.name "GitHub Actions" + git config --global user.email "actions@github.com" + git commit -m "chore: apply lint fixes [skip ci]" + git push + build-scan: name: Build and Security Scan runs-on: ubuntu-latest needs: lint-test + permissions: + contents: read + checks: write + security-events: write steps: - uses: actions/checkout@v4 From fdd6ba8eaf0d192dd66323a1d966ef145b8329ab Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Tue, 29 Apr 2025 14:07:30 +0200 Subject: [PATCH 277/324] CI/CD: Fix test pipeline --- .github/workflows/ci.yml | 8 ++++++-- biome.json | 2 +- src/index.ts | 2 +- src/tests/helper.ts | 5 ++++- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 501b994..d8299b5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - + - name: Setup Bun uses: oven-sh/setup-bun@v2 with: @@ -39,7 +39,11 @@ jobs: git diff --cached --quiet || echo "changes_detected=true" >> $GITHUB_OUTPUT - name: Run unit tests - run: bun test --reporter=junit --reporter-outfile=./unit-test.xml + run: | + export DOCKSTATAPI_PORT=5971 + bun clean + bun test --reporter=junit --reporter-outfile=./unit-test.xml + bun clean - name: Publish Test Report uses: mikepenz/action-junit-report@v5 diff --git a/biome.json b/biome.json index a02c1a3..bdb5ecc 100644 --- a/biome.json +++ b/biome.json @@ -3,7 +3,7 @@ "vcs": { "enabled": true, "clientKind": "git", - "useIgnoreFile": false + "useIgnoreFile": true }, "formatter": { "enabled": true, diff --git a/src/index.ts b/src/index.ts index 8090d9a..0bc6a37 100644 --- a/src/index.ts +++ b/src/index.ts @@ -145,7 +145,7 @@ async function startServer() { } try { - DockStatAPI.listen(3000, ({ hostname, port }) => { + DockStatAPI.listen(process.env.DOCKSTATAPI_PORT || 3000, ({ hostname, port }) => { console.log("----- [ ############## ]"); logger.info(`DockStatAPI is running at http://${hostname}:${port}`); logger.info( diff --git a/src/tests/helper.ts b/src/tests/helper.ts index fabc45b..6e816b4 100644 --- a/src/tests/helper.ts +++ b/src/tests/helper.ts @@ -5,7 +5,10 @@ import { logger } from "~/core/utils/logger"; import { DockStatAPI } from ".."; export const API_KEY = "TestKey"; -const server = "http://localhost:3001"; + +const host = "http://localhost"; +const port = process.env.DOCKSTATAPI_PORT || 3000; +const server = `${host}:${port}` export async function runTestResponse( path: string, From dc7e7a503c5a5ab7445ca843adc4e3d5638e2d8c Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Tue, 29 Apr 2025 14:09:53 +0200 Subject: [PATCH 278/324] CI/CD: Fix Linter --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8299b5..8e15465 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: run: bun install - name: Run linter - run: bun run biome ci + run: bun run biome lint --fix - name: Add linted files run: git add src/ From df40a845f4e062b42bda849693fbe91f83351811 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Tue, 29 Apr 2025 14:12:36 +0200 Subject: [PATCH 279/324] CI/CD: Disable padding in github logs --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8e15465..fe6155a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,9 @@ jobs: run: bun install - name: Run linter - run: bun run biome lint --fix + run: | + export PAD_NEW_LINES=false + bun run biome lint --fix - name: Add linted files run: git add src/ From 76ad637dca3b724a081a285870ac05ea27927f3e Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Tue, 29 Apr 2025 14:13:41 +0200 Subject: [PATCH 280/324] CI/CD: Disable padding in github logs --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fe6155a..e07e63a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,6 @@ jobs: - name: Run linter run: | - export PAD_NEW_LINES=false bun run biome lint --fix - name: Add linted files @@ -43,6 +42,7 @@ jobs: - name: Run unit tests run: | export DOCKSTATAPI_PORT=5971 + export PAD_NEW_LINES=false bun clean bun test --reporter=junit --reporter-outfile=./unit-test.xml bun clean From f2b58f26827daf0e29a823c252232b498cbf2bfa Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Tue, 29 Apr 2025 14:23:46 +0200 Subject: [PATCH 281/324] CI/CD: Forgot to activate the socket proxy in ci --- .github/workflows/ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e07e63a..0dbe22b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,6 +27,10 @@ jobs: - name: Install dependencies run: bun install + - name: Knip check + if: ${{ github.event_name == 'pull_request' }} + uses: codex-/knip-reporter@v2 + - name: Run linter run: | bun run biome lint --fix @@ -43,6 +47,7 @@ jobs: run: | export DOCKSTATAPI_PORT=5971 export PAD_NEW_LINES=false + docker compose -f docker/docker-compose.dev.yaml up -d bun clean bun test --reporter=junit --reporter-outfile=./unit-test.xml bun clean From 566837751e19959545132cb6753f5fd1334107e4 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 29 Apr 2025 14:38:51 +0200 Subject: [PATCH 282/324] Update ci.yml --- .github/workflows/ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0dbe22b..c0d2b81 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,10 @@ jobs: - name: Run linter run: | - bun run biome lint --fix + bun biome format --fix + bun biome lint --fix + bun biome check --fix + bun biome ci --fix - name: Add linted files run: git add src/ From 30618fbebbe507c746c455d863a67be303245776 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 29 Apr 2025 14:45:17 +0200 Subject: [PATCH 283/324] Update ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c0d2b81..72929a7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: bun biome format --fix bun biome lint --fix bun biome check --fix - bun biome ci --fix + bun biome ci - name: Add linted files run: git add src/ From 205c1e4865c978a35410f3d42bf4eeee78c8afd9 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Tue, 29 Apr 2025 12:45:37 +0000 Subject: [PATCH 284/324] chore: apply lint fixes [skip ci] --- src/index.ts | 23 +++++++++++++---------- src/tests/helper.ts | 2 +- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/index.ts b/src/index.ts index 0bc6a37..8d19f44 100644 --- a/src/index.ts +++ b/src/index.ts @@ -145,16 +145,19 @@ async function startServer() { } try { - DockStatAPI.listen(process.env.DOCKSTATAPI_PORT || 3000, ({ hostname, port }) => { - console.log("----- [ ############## ]"); - logger.info(`DockStatAPI is running at http://${hostname}:${port}`); - logger.info( - `Swagger API Documentation available at http://${hostname}:${port}/swagger`, - ); - logger.info(`License: ${license}`); - logger.info(`Author: ${authorWebsite}`); - logger.info(`Contributors: ${contributors}`); - }); + DockStatAPI.listen( + process.env.DOCKSTATAPI_PORT || 3000, + ({ hostname, port }) => { + console.log("----- [ ############## ]"); + logger.info(`DockStatAPI is running at http://${hostname}:${port}`); + logger.info( + `Swagger API Documentation available at http://${hostname}:${port}/swagger`, + ); + logger.info(`License: ${license}`); + logger.info(`Author: ${authorWebsite}`); + logger.info(`Contributors: ${contributors}`); + }, + ); } catch (error) { logger.error("Failed to start server:", error); process.exit(1); diff --git a/src/tests/helper.ts b/src/tests/helper.ts index 6e816b4..6016111 100644 --- a/src/tests/helper.ts +++ b/src/tests/helper.ts @@ -8,7 +8,7 @@ export const API_KEY = "TestKey"; const host = "http://localhost"; const port = process.env.DOCKSTATAPI_PORT || 3000; -const server = `${host}:${port}` +const server = `${host}:${port}`; export async function runTestResponse( path: string, From de68073f39f884d77d37f146db40090253fda312 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 30 Apr 2025 19:03:56 +0200 Subject: [PATCH 285/324] Feat: Update swagger api with better examples and added submodule for types --- .github/workflows/cd.yml | 1 - .github/workflows/ci.yml | 14 +- .gitmodules | 3 + package.json | 9 +- src/core/database/logs.ts | 32 ++- src/core/utils/logger.ts | 31 +- src/index.ts | 4 +- src/routes/api-config.ts | 353 ++++++++++++++++++++++- src/routes/docker-manager.ts | 151 ++++++++++ src/routes/docker-stats.ts | 159 +++++++++++ src/routes/live-logs.ts | 2 +- src/routes/logs.ts | 166 +++++++++++ src/routes/stacks.ts | 320 +++++++++++++++++++++ src/routes/utils.ts | 75 +++++ src/typings | 1 + src/typings/database.ts | 27 -- src/typings/docker-compose.ts | 522 ---------------------------------- src/typings/docker.ts | 41 --- src/typings/dockerode.ts | 162 ----------- src/typings/elysiajs.ts | 12 - src/typings/misc.ts | 5 - src/typings/plugin.ts | 26 -- src/typings/websocket.ts | 15 - 23 files changed, 1278 insertions(+), 853 deletions(-) create mode 100644 .gitmodules create mode 160000 src/typings delete mode 100644 src/typings/database.ts delete mode 100644 src/typings/docker-compose.ts delete mode 100644 src/typings/docker.ts delete mode 100644 src/typings/dockerode.ts delete mode 100644 src/typings/elysiajs.ts delete mode 100644 src/typings/misc.ts delete mode 100644 src/typings/plugin.ts delete mode 100644 src/typings/websocket.ts diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 15f58c4..1b60b66 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -12,7 +12,6 @@ jobs: publish: name: Publish Container Image runs-on: ubuntu-latest - environment: production steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 72929a7..f1c83b2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,7 +58,7 @@ jobs: - name: Publish Test Report uses: mikepenz/action-junit-report@v5 with: - report_paths: 'unit-test.xml' + report_paths: "unit-test.xml" - name: Commit and push lint changes if: | @@ -67,7 +67,7 @@ jobs: run: | git config --global user.name "GitHub Actions" git config --global user.email "actions@github.com" - git commit -m "chore: apply lint fixes [skip ci]" + git commit -m "CQL: Apply lint fixes [skip ci]" git push build-scan: @@ -103,12 +103,12 @@ jobs: - name: Trivy vulnerability scan uses: aquasecurity/trivy-action@0.28.0 with: - image-ref: 'dockstatapi:ci-${{ github.sha }}' - format: 'sarif' - output: 'trivy-results.sarif' - severity: 'HIGH,CRITICAL' + image-ref: "dockstatapi:ci-${{ github.sha }}" + format: "sarif" + output: "trivy-results.sarif" + severity: "HIGH,CRITICAL" - name: Upload security results uses: github/codeql-action/upload-sarif@v3 with: - sarif_file: 'trivy-results.sarif' + sarif_file: "trivy-results.sarif" diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..7132207 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "src/typings"] + path = src/typings + url = git@github.com:Its4Nik/dockstat-types.git diff --git a/package.json b/package.json index 8b64f69..6bf8d3b 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "clean:win": "node -e \"process.exit(process.platform === 'win32' ? 0 : 1)\" && cmd /c del /Q data/dockstatapi*.db* && echo 'success'", "clean:lin": "node -e \"process.exit(process.platform !== 'win32' ? 0 : 1)\" && rm -f data/dockstatapi*.db* && echo 'success'", "knip": "knip", - "lint": "biome check --formatter-enabled=true --linter-enabled=true --organize-imports-enabled=true --fix src" + "lint": "biome check --formatter-enabled=true --linter-enabled=true --organize-imports-enabled=true --fix src", + "test": "bun test src/tests/**/*.test.ts" }, "dependencies": { "@elysiajs/server-timing": "^1.2.1", @@ -47,5 +48,7 @@ "wrap-ansi": "^9.0.0" }, "module": "src/index.js", - "trustedDependencies": ["protobufjs"] -} + "trustedDependencies": [ + "protobufjs" + ] +} \ No newline at end of file diff --git a/src/core/database/logs.ts b/src/core/database/logs.ts index 3fba8dc..ad100c4 100644 --- a/src/core/database/logs.ts +++ b/src/core/database/logs.ts @@ -16,6 +16,16 @@ const stmt = { deleteByLevel: db.prepare("DELETE FROM backend_log_entries WHERE level = ?"), }; +function convertToLogMessage(row: any): log_message { + return { + level: row.level, + timestamp: row.timestamp, + message: row.message, + file: row.file, + line: row.line, + }; +} + export function addLogEntry(data: log_message) { return executeDbOperation( "Add Log Entry", @@ -44,17 +54,17 @@ export function addLogEntry(data: log_message) { ); } -export function getAllLogs() { - return executeDbOperation("Get All Logs", () => stmt.selectAll.all()); +export function getAllLogs(): log_message[] { + return executeDbOperation( + "Get All Logs", + () => stmt.selectAll.all().map(convertToLogMessage), + ); } -export function getLogsByLevel(level: string) { +export function getLogsByLevel(level: string): log_message[] { return executeDbOperation( "Get Logs By Level", - () => stmt.selectByLevel.all(level), - () => { - if (typeof level !== "string") throw new TypeError("Invalid level type"); - }, + () => stmt.selectByLevel.all(level).map(convertToLogMessage), ); } @@ -63,11 +73,7 @@ export function clearAllLogs() { } export function clearLogsByLevel(level: string) { - return executeDbOperation( - "Clear Logs By Level", - () => stmt.deleteByLevel.run(level), - () => { - if (typeof level !== "string") throw new TypeError("Invalid level type"); - }, + return executeDbOperation("Clear Logs By Level", () => + stmt.deleteByLevel.run(level), ); } diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts index 5320876..30e6045 100644 --- a/src/core/utils/logger.ts +++ b/src/core/utils/logger.ts @@ -64,9 +64,21 @@ const levelColors: Record = { ut: chalk.hex("#9D00FF"), }; +const parseTimestamp = (timestamp: string): string => { + const [datePart, timePart] = timestamp.split(" "); + const [day, month] = datePart.split("/"); + const [hours, minutes, seconds] = timePart.split(":"); + const year = new Date().getFullYear(); + const date = new Date(year, parseInt(month) - 1, parseInt(day), parseInt(hours), parseInt(minutes), parseInt(seconds)); + return date.toISOString(); +}; + const handleWebSocketLog = (log: log_message) => { try { - logToClients(log); + logToClients({ + ...log, + timestamp: parseTimestamp(log.timestamp) + }); } catch (error) { console.error( `WebSocket logging failed: ${ @@ -81,7 +93,10 @@ const handleDatabaseLog = (log: log_message): void => { return; } try { - dbFunctions.addLogEntry(log); + dbFunctions.addLogEntry({ + ...log, + timestamp: parseTimestamp(log.timestamp) + }); } catch (error) { console.error( `Database logging failed: ${ @@ -160,17 +175,17 @@ export const logger = createLogger({ handleDatabaseLog({ level: processedLevel, - timestamp, + timestamp: timestamp, message: processedMessage, - file, - line, + file: file, + line: line, }); handleWebSocketLog({ level: processedLevel, - timestamp, + timestamp: timestamp, message: processedMessage, - file, - line, + file: file, + line: line, }); return `${coloredLevel} [ ${coloredTimestamp} ] - ${formattedMessage}`; diff --git a/src/index.ts b/src/index.ts index 8d19f44..062d8b8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -76,7 +76,7 @@ export const DockStatAPI = new Elysia() { name: "Utils", description: "Various utilities which might be useful", - }, + } ], }, }), @@ -84,7 +84,7 @@ export const DockStatAPI = new Elysia() .onBeforeHandle(async (context) => { const { path, request, set } = context; - if (path === "/health" || path.startsWith("/swagger")) { + if (path === "/health" || path.startsWith("/swagger") || path.startsWith("/trpc")) { logger.info(`Requested unguarded route: ${path}`); return; } diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index 5be018c..354ffb0 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -44,6 +44,48 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) tags: ["Management"], description: "Returns current API configuration including data retention policies and security settings", + responses: { + "200": { + description: "Successfully retrieved configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + fetching_interval: { + type: "number", + example: 5 + }, + keep_data_for: { + type: "number", + example: 7 + }, + api_key: { + type: "string", + example: "hashed_api_key" + } + } + } + } + } + }, + "400": { + description: "Error retrieving configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error getting the DockStatAPI config" + } + } + } + } + } + } + } }, }, ) @@ -65,6 +107,51 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) tags: ["Management"], description: "Lists all active plugins with their registration details and status", + responses: { + "200": { + description: "Successfully retrieved plugins", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "object", + properties: { + name: { + type: "string", + example: "example-plugin" + }, + version: { + type: "string", + example: "1.0.0" + }, + status: { + type: "string", + example: "active" + } + } + } + } + } + } + }, + "400": { + description: "Error retrieving plugins", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error getting all registered plugins" + } + } + } + } + } + } + } }, }, ) @@ -89,16 +176,50 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) } }, { - body: t.Object({ - fetching_interval: t.Number(), - keep_data_for: t.Number(), - api_key: t.String(), - }), detail: { tags: ["Management"], description: "Modifies core API settings including data collection intervals, retention periods, and security credentials", + responses: { + "200": { + description: "Successfully updated configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Updated DockStatAPI config" + } + } + } + } + } + }, + "400": { + description: "Error updating configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error updating the DockStatAPI config" + } + } + } + } + } + } + } }, + body: t.Object({ + fetching_interval: t.Number(), + keep_data_for: t.Number(), + api_key: t.String(), + }), }, ) .get( @@ -130,6 +251,81 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) tags: ["Management"], description: "Displays package metadata including dependencies, contributors, and licensing information", + responses: { + "200": { + description: "Successfully retrieved package information", + content: { + "application/json": { + schema: { + type: "object", + properties: { + version: { + type: "string", + example: "3.0.0" + }, + description: { + type: "string", + example: "DockStatAPI is an API backend featuring plugins and more for DockStat" + }, + license: { + type: "string", + example: "CC BY-NC 4.0" + }, + authorName: { + type: "string", + example: "ItsNik" + }, + authorEmail: { + type: "string", + example: "info@itsnik.de" + }, + authorWebsite: { + type: "string", + example: "https://github.com/Its4Nik" + }, + contributors: { + type: "array", + items: { + type: "string" + }, + example: [] + }, + dependencies: { + type: "object", + example: { + "@elysiajs/server-timing": "^1.2.1", + "@elysiajs/static": "^1.2.0" + } + }, + devDependencies: { + type: "object", + example: { + "@biomejs/biome": "1.9.4", + "@types/dockerode": "^3.3.38" + } + } + } + } + } + } + }, + "400": { + description: "Error retrieving package information", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error while reading package.json" + } + } + } + } + } + } + } }, }, ) @@ -147,6 +343,40 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) detail: { tags: ["Management"], description: "Backs up the internal database", + responses: { + "200": { + description: "Successfully created backup", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "backup_2024-03-20_12-00-00.db.bak" + } + } + } + } + } + }, + "400": { + description: "Error creating backup", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error backing up" + } + } + } + } + } + } + } }, }, ) @@ -177,6 +407,41 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) detail: { tags: ["Management"], description: "Lists all available backups", + responses: { + "200": { + description: "Successfully retrieved backup list", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "string" + }, + example: [ + "backup_2024-03-20_12-00-00.db.bak", + "backup_2024-03-19_12-00-00.db.bak" + ] + } + } + } + }, + "400": { + description: "Error retrieving backup list", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Reading Backup directory" + } + } + } + } + } + } + } }, }, ) @@ -205,14 +470,52 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) } }, { - query: t.Object({ - filename: t.Optional(t.String()), - }), detail: { tags: ["Management"], description: "Download a specific backup or the latest if no filename is provided", + responses: { + "200": { + description: "Successfully downloaded backup file", + content: { + "application/octet-stream": { + schema: { + type: "string", + format: "binary", + example: "Binary backup file content" + } + } + }, + headers: { + "Content-Disposition": { + schema: { + type: "string", + example: "attachment; filename=\"backup_2024-03-20_12-00-00.db.bak\"" + } + } + } + }, + "400": { + description: "Error downloading backup", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Backup download failed" + } + } + } + } + } + } + } }, + query: t.Object({ + filename: t.Optional(t.String()), + }), }, ) .post( @@ -252,6 +555,40 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) detail: { tags: ["Management"], description: "Restore database from uploaded backup file", + responses: { + "200": { + description: "Successfully restored database", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Database restored successfully" + } + } + } + } + } + }, + "400": { + description: "Error restoring database", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Database restoration error" + } + } + } + } + } + } + } }, }, ); diff --git a/src/routes/docker-manager.ts b/src/routes/docker-manager.ts index 8caadd2..13f3312 100644 --- a/src/routes/docker-manager.ts +++ b/src/routes/docker-manager.ts @@ -27,6 +27,40 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) tags: ["Management"], description: "Registers a new Docker host to the monitoring system with connection details", + responses: { + "200": { + description: "Successfully added Docker host", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Added docker host (Localhost)" + } + } + } + } + } + }, + "400": { + description: "Error adding Docker host", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error adding docker Host" + } + } + } + } + } + } + } }, body: t.Object({ name: t.String(), @@ -56,6 +90,40 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) tags: ["Management"], description: "Modifies existing Docker host configuration parameters (name, address, security)", + responses: { + "200": { + description: "Successfully updated Docker host", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Updated docker host (1)" + } + } + } + } + } + }, + "400": { + description: "Error updating Docker host", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to update host" + } + } + } + } + } + } + } }, body: t.Object({ id: t.Number(), @@ -87,6 +155,55 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) tags: ["Management"], description: "Lists all configured Docker hosts with their connection settings", + responses: { + "200": { + description: "Successfully retrieved Docker hosts", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "object", + properties: { + id: { + type: "number", + example: 1 + }, + name: { + type: "string", + example: "Localhost" + }, + hostAddress: { + type: "string", + example: "localhost:2375" + }, + secure: { + type: "boolean", + example: false + } + } + } + } + } + } + }, + "400": { + description: "Error retrieving Docker hosts", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve hosts" + } + } + } + } + } + } + } }, }, ) @@ -111,6 +228,40 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) tags: ["Management"], description: "Removes Docker host from monitoring system and clears associated data", + responses: { + "200": { + description: "Successfully deleted Docker host", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Deleted docker host (1)" + } + } + } + } + } + }, + "400": { + description: "Error deleting Docker host", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to delete host" + } + } + } + } + } + } + } }, params: t.Object({ id: t.Number(), diff --git a/src/routes/docker-stats.ts b/src/routes/docker-stats.ts index d804afa..db10816 100644 --- a/src/routes/docker-stats.ts +++ b/src/routes/docker-stats.ts @@ -108,6 +108,76 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) tags: ["Statistics"], description: "Collects real-time statistics for all Docker containers across monitored hosts, including CPU and memory utilization", + responses: { + "200": { + description: "Successfully retrieved container statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + containers: { + type: "array", + items: { + type: "object", + properties: { + id: { + type: "string", + example: "abc123def456" + }, + hostId: { + type: "string", + example: "1" + }, + name: { + type: "string", + example: "example-container" + }, + image: { + type: "string", + example: "nginx:latest" + }, + status: { + type: "string", + example: "running" + }, + state: { + type: "string", + example: "running" + }, + cpuUsage: { + type: "number", + example: 0.5 + }, + memoryUsage: { + type: "number", + example: 1024 + } + } + } + } + } + } + } + } + }, + "400": { + description: "Error retrieving container statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve containers" + } + } + } + } + } + } + } }, }, ) @@ -161,6 +231,95 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) tags: ["Statistics"], description: "Provides detailed system metrics and Docker runtime information for specified host", + responses: { + "200": { + description: "Successfully retrieved host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + hostId: { + type: "number", + example: 1 + }, + hostName: { + type: "string", + example: "Localhost" + }, + dockerVersion: { + type: "string", + example: "24.0.5" + }, + apiVersion: { + type: "string", + example: "1.41" + }, + os: { + type: "string", + example: "Linux" + }, + architecture: { + type: "string", + example: "x86_64" + }, + totalMemory: { + type: "number", + example: 16777216 + }, + totalCPU: { + type: "number", + example: 4 + }, + labels: { + type: "array", + items: { + type: "string" + }, + example: ["environment=production"] + }, + images: { + type: "number", + example: 10 + }, + containers: { + type: "number", + example: 5 + }, + containersPaused: { + type: "number", + example: 0 + }, + containersRunning: { + type: "number", + example: 4 + }, + containersStopped: { + type: "number", + example: 1 + } + } + } + } + } + }, + "400": { + description: "Error retrieving host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve host config" + } + } + } + } + } + } + } }, }, ); diff --git a/src/routes/live-logs.ts b/src/routes/live-logs.ts index 2e894b6..50e9ea8 100644 --- a/src/routes/live-logs.ts +++ b/src/routes/live-logs.ts @@ -11,7 +11,7 @@ const activeConnections = new Set>(); export const liveLogs = new Elysia({ prefix: "/logs" }).ws("/ws", { open(ws) { activeConnections.add(ws); - ws.send({ message: "Connection established" }); + ws.send({ message: "Connection established", level: "info", timestamp: new Date().toISOString(), file: "live-logs.ts", line: 14 }); logger.info(`New Logs WebSocket established (${ws.id})`); }, close(ws) { diff --git a/src/routes/logs.ts b/src/routes/logs.ts index 626e723..20b3be2 100644 --- a/src/routes/logs.ts +++ b/src/routes/logs.ts @@ -23,6 +23,55 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) tags: ["Management"], description: "Retrieves complete application log history from persistent storage", + responses: { + "200": { + description: "Successfully retrieved logs", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "object", + properties: { + id: { + type: "number", + example: 1 + }, + level: { + type: "string", + example: "info" + }, + message: { + type: "string", + example: "Application started" + }, + timestamp: { + type: "string", + example: "2024-03-20T12:00:00Z" + } + } + } + } + } + } + }, + "500": { + description: "Error retrieving logs", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve logs" + } + } + } + } + } + } + } }, }, ) @@ -46,6 +95,55 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) tags: ["Management"], description: "Filters logs by severity level (debug, info, warn, error, fatal)", + responses: { + "200": { + description: "Successfully retrieved logs by level", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "object", + properties: { + id: { + type: "number", + example: 1 + }, + level: { + type: "string", + example: "info" + }, + message: { + type: "string", + example: "Application started" + }, + timestamp: { + type: "string", + example: "2024-03-20T12:00:00Z" + } + } + } + } + } + } + }, + "500": { + description: "Error retrieving logs", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve logs" + } + } + } + } + } + } + } }, }, ) @@ -68,6 +166,40 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) detail: { tags: ["Management"], description: "Purges all historical log records from the database", + responses: { + "200": { + description: "Successfully cleared all logs", + content: { + "application/json": { + schema: { + type: "object", + properties: { + success: { + type: "boolean", + example: true + } + } + } + } + } + }, + "500": { + description: "Error clearing logs", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Could not delete all logs" + } + } + } + } + } + } + } }, }, ) @@ -90,6 +222,40 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) detail: { tags: ["Management"], description: "Clears log entries matching specified severity level", + responses: { + "200": { + description: "Successfully cleared logs by level", + content: { + "application/json": { + schema: { + type: "object", + properties: { + success: { + type: "boolean", + example: true + } + } + } + } + } + }, + "500": { + description: "Error clearing logs", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve logs" + } + } + } + } + } + } + } }, }, ); diff --git a/src/routes/stacks.ts b/src/routes/stacks.ts index 56c8d1b..52eaf86 100644 --- a/src/routes/stacks.ts +++ b/src/routes/stacks.ts @@ -68,6 +68,40 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) tags: ["Stacks"], description: "Deploys a new Docker stack using a provided compose specification, allowing custom configurations and image updates", + responses: { + "200": { + description: "Successfully deployed stack", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Stack example-stack deployed successfully" + } + } + } + } + } + }, + "400": { + description: "Error deploying stack", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error deploying stack" + } + } + } + } + } + } + } }, body: t.Object({ compose_spec: t.Any(), @@ -105,6 +139,40 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) tags: ["Stacks"], description: "Initiates a Docker stack, starting all associated containers", + responses: { + "200": { + description: "Successfully started stack", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Stack 1 started successfully" + } + } + } + } + } + }, + "400": { + description: "Error starting stack", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error starting stack" + } + } + } + } + } + } + } }, body: t.Object({ stackId: t.Number(), @@ -135,6 +203,40 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) tags: ["Stacks"], description: "Halts a running Docker stack and its containers while preserving configurations", + responses: { + "200": { + description: "Successfully stopped stack", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Stack 1 stopped successfully" + } + } + } + } + } + }, + "400": { + description: "Error stopping stack", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error stopping stack" + } + } + } + } + } + } + } }, body: t.Object({ stackId: t.Number(), @@ -165,6 +267,40 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) tags: ["Stacks"], description: "Performs full stack restart - stops and restarts all stack components in sequence", + responses: { + "200": { + description: "Successfully restarted stack", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Stack 1 restarted successfully" + } + } + } + } + } + }, + "400": { + description: "Error restarting stack", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error restarting stack" + } + } + } + } + } + } + } }, body: t.Object({ stackId: t.Number(), @@ -195,6 +331,40 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) tags: ["Stacks"], description: "Updates container images for a stack using Docker's pull mechanism (requires stack ID)", + responses: { + "200": { + description: "Successfully pulled images", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Images for stack 1 pulled successfully" + } + } + } + } + } + }, + "400": { + description: "Error pulling images", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error pulling images" + } + } + } + } + } + } + } }, body: t.Object({ stackId: t.Number(), @@ -236,6 +406,69 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) tags: ["Stacks"], description: "Retrieves operational status for either a specific stack (by ID) or all managed stacks", + responses: { + "200": { + description: "Successfully retrieved stack status", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Stack 1 status retrieved successfully" + }, + status: { + type: "object", + properties: { + name: { + type: "string", + example: "example-stack" + }, + status: { + type: "string", + example: "running" + }, + containers: { + type: "array", + items: { + type: "object", + properties: { + name: { + type: "string", + example: "example-stack_web_1" + }, + status: { + type: "string", + example: "running" + } + } + } + } + } + } + } + } + } + } + }, + "400": { + description: "Error getting stack status", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error getting stack status" + } + } + } + } + } + } + } }, query: t.Object({ stackId: t.Number(), @@ -260,6 +493,59 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) tags: ["Stacks"], description: "Lists all registered stacks with their complete configuration details", + responses: { + "200": { + description: "Successfully retrieved stacks", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "object", + properties: { + id: { + type: "number", + example: 1 + }, + name: { + type: "string", + example: "example-stack" + }, + version: { + type: "number", + example: 1 + }, + source: { + type: "string", + example: "github.com/example/repo" + }, + automatic_reboot_on_error: { + type: "boolean", + example: true + } + } + } + } + } + } + }, + "400": { + description: "Error getting stacks", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error getting stacks" + } + } + } + } + } + } + } }, }, ) @@ -283,6 +569,40 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) tags: ["Stacks"], description: "Permanently removes a stack configuration and cleans up associated resources", + responses: { + "200": { + description: "Successfully deleted stack", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Stack 1 deleted successfully" + } + } + } + } + } + }, + "400": { + description: "Error deleting stack", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error deleting stack" + } + } + } + } + } + } + } }, body: t.Object({ stackId: t.Number(), diff --git a/src/routes/utils.ts b/src/routes/utils.ts index b578f92..0f0ca26 100644 --- a/src/routes/utils.ts +++ b/src/routes/utils.ts @@ -42,6 +42,81 @@ export const utilRoutes = new Elysia({ prefix: "/utils" }).get( tags: ["Utils"], description: "Retrieves DockStatAPI metadata including version, author information, dependencies, and licensing details", + responses: { + "200": { + description: "Successfully retrieved API information", + content: { + "application/json": { + schema: { + type: "object", + properties: { + version: { + type: "string", + example: "3.0.0" + }, + authorEmail: { + type: "string", + example: "info@itsnik.de" + }, + authorName: { + type: "string", + example: "ItsNik" + }, + authorWebsite: { + type: "string", + example: "https://github.com/Its4Nik" + }, + contributors: { + type: "array", + items: { + type: "string" + }, + example: [] + }, + dependencies: { + type: "object", + example: { + "@elysiajs/server-timing": "^1.2.1", + "@elysiajs/static": "^1.2.0" + } + }, + description: { + type: "string", + example: "DockStatAPI is an API backend featuring plugins and more for DockStat" + }, + devDependencies: { + type: "object", + example: { + "@biomejs/biome": "1.9.4", + "@types/dockerode": "^3.3.38" + } + }, + license: { + type: "string", + example: "CC BY-NC 4.0" + } + } + } + } + } + }, + "400": { + description: "Error retrieving API information", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error getting DockStatAPI information" + } + } + } + } + } + } + } }, }, ); diff --git a/src/typings b/src/typings new file mode 160000 index 0000000..9cae829 --- /dev/null +++ b/src/typings @@ -0,0 +1 @@ +Subproject commit 9cae829bead60cd13351b757340f3225649cb11d diff --git a/src/typings/database.ts b/src/typings/database.ts deleted file mode 100644 index 880a801..0000000 --- a/src/typings/database.ts +++ /dev/null @@ -1,27 +0,0 @@ -interface config { - keep_data_for: number; - fetching_interval: number; - api_key: string; -} - -interface stacks_config { - id: number; - name: string; - version: number; - custom: boolean; - source: string; - container_count: number; - stack_prefix: string; - automatic_reboot_on_error: boolean; - image_updates: boolean; -} - -interface log_message { - level: string; - timestamp: string; - message: string; - file: string; - line: number; -} - -export type { config, stacks_config, log_message }; diff --git a/src/typings/docker-compose.ts b/src/typings/docker-compose.ts deleted file mode 100644 index 8e6a5f9..0000000 --- a/src/typings/docker-compose.ts +++ /dev/null @@ -1,522 +0,0 @@ -export interface Stack { - compose_spec: ComposeSpec; - name: string; - version: number; - source: string; - id?: number; -} - -export interface ComposeSpec { - version?: string; - name?: string; - include?: Include[]; - services?: { [key: string]: Service }; - networks?: { [key: string]: Network }; - volumes?: { [key: string]: Volume }; - secrets?: { [key: string]: Secret }; - configs?: { [key: string]: Config }; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; -} - -type Include = - | string - | { - path: string | string[]; - env_file?: string | string[]; - project_directory?: string; - }; - -interface Service { - develop?: Development | null; - deploy?: Deployment | null; - annotations?: ListOrDict; - attach?: boolean | string; - build?: - | string - | { - context?: string; - dockerfile?: string; - dockerfile_inline?: string; - entitlements?: string[]; - args?: ListOrDict; - ssh?: ListOrDict; - labels?: ListOrDict; - cache_from?: string[]; - cache_to?: string[]; - no_cache?: boolean | string; - additional_contexts?: ListOrDict; - network?: string; - pull?: boolean | string; - target?: string; - shm_size?: number | string; - extra_hosts?: ExtraHosts; - isolation?: string; - privileged?: boolean | string; - secrets?: ServiceConfigOrSecret[]; - tags?: string[]; - ulimits?: Ulimits; - platforms?: string[]; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }; - blkio_config?: { - device_read_bps?: BlkioLimit[]; - device_read_iops?: BlkioLimit[]; - device_write_bps?: BlkioLimit[]; - device_write_iops?: BlkioLimit[]; - weight?: number | string; - weight_device?: BlkioWeight[]; - }; - cap_add?: string[]; - cap_drop?: string[]; - cgroup?: "host" | "private"; - cgroup_parent?: string; - command?: Command; - configs?: ServiceConfigOrSecret[]; - container_name?: string; - cpu_count?: string | number; - cpu_percent?: string | number; - cpu_shares?: number | string; - cpu_quota?: number | string; - cpu_period?: number | string; - cpu_rt_period?: number | string; - cpu_rt_runtime?: number | string; - cpus?: number | string; - cpuset?: string; - credential_spec?: { - config?: string; - file?: string; - registry?: string; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }; - depends_on?: - | string[] - | { - [service: string]: { - condition: - | "service_started" - | "service_healthy" - | "service_completed_successfully"; - restart?: boolean | string; - required?: boolean; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }; - }; - device_cgroup_rules?: string[]; - devices?: ( - | string - | { - source: string; - target?: string; - permissions?: string; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - } - )[]; - dns?: StringOrList; - dns_opt?: string[]; - dns_search?: StringOrList; - domainname?: string; - entrypoint?: Command; - env_file?: EnvFile; - label_file?: string | string[]; - environment?: ListOrDict; - expose?: (string | number)[]; - extends?: string | { service: string; file?: string }; - external_links?: string[]; - extra_hosts?: ExtraHosts; - gpus?: - | "all" - | Array<{ - capabilities?: string[]; - count?: string | number; - device_ids?: string[]; - driver?: string; - options?: ListOrDict; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }>; - group_add?: (string | number)[]; - healthcheck?: Healthcheck; - hostname?: string; - image?: string; - init?: boolean | string; - ipc?: string; - isolation?: string; - labels?: ListOrDict; - links?: string[]; - logging?: { - driver?: string; - options?: { [key: string]: string | number | null }; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }; - mac_address?: string; - mem_limit?: number | string; - mem_reservation?: string | number; - mem_swappiness?: number | string; - memswap_limit?: number | string; - network_mode?: string; - networks?: - | string[] - | { - [network: string]: { - aliases?: string[]; - ipv4_address?: string; - ipv6_address?: string; - link_local_ips?: string[]; - mac_address?: string; - driver_opts?: { [key: string]: string | number }; - priority?: number; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - } | null; - }; - oom_kill_disable?: boolean | string; - oom_score_adj?: string | number; - pid?: string | null; - pids_limit?: number | string; - platform?: string; - ports?: ( - | number - | string - | { - name?: string; - mode?: string; - host_ip?: string; - target?: number | string; - published?: string | number; - protocol?: string; - app_protocol?: string; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - } - )[]; - post_start?: ServiceHook[]; - pre_stop?: ServiceHook[]; - privileged?: boolean | string; - profiles?: string[]; - pull_policy?: "always" | "never" | "if_not_present" | "build" | "missing"; - read_only?: boolean | string; - restart?: string; - runtime?: string; - scale?: number | string; - security_opt?: string[]; - shm_size?: number | string; - secrets?: ServiceConfigOrSecret[]; - sysctls?: ListOrDict; - stdin_open?: boolean | string; - stop_grace_period?: string; - stop_signal?: string; - storage_opt?: object; - tmpfs?: StringOrList; - tty?: boolean | string; - ulimits?: Ulimits; - user?: string; - uts?: string; - userns_mode?: string; - volumes?: ( - | string - | { - type: string; - source?: string; - target?: string; - read_only?: boolean | string; - consistency?: string; - bind?: { - propagation?: string; - create_host_path?: boolean | string; - recursive?: "enabled" | "disabled" | "writable" | "readonly"; - selinux?: "z" | "Z"; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }; - volume?: { - nocopy?: boolean | string; - subpath?: string; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }; - tmpfs?: { - size?: number | string; - mode?: number | string; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - } - )[]; - volumes_from?: string[]; - working_dir?: string; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; -} - -interface Healthcheck { - disable?: boolean | string; - interval?: string; - retries?: number | string; - test?: string | string[]; - timeout?: string; - start_period?: string; - start_interval?: string; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; -} - -interface Development { - watch?: Array<{ - path: string; - action: "rebuild" | "sync" | "restart" | "sync+restart" | "sync+exec"; - ignore?: string[]; - target?: string; - exec?: ServiceHook; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }>; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; -} - -interface Deployment { - mode?: string; - endpoint_mode?: string; - replicas?: number | string; - labels?: ListOrDict; - rollback_config?: { - parallelism?: number | string; - delay?: string; - failure_action?: string; - monitor?: string; - max_failure_ratio?: number | string; - order?: "start-first" | "stop-first"; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }; - update_config?: { - parallelism?: number | string; - delay?: string; - failure_action?: string; - monitor?: string; - max_failure_ratio?: number | string; - order?: "start-first" | "stop-first"; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }; - resources?: { - limits?: { - cpus?: number | string; - memory?: string; - pids?: number | string; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }; - reservations?: { - cpus?: number | string; - memory?: string; - generic_resources?: Array<{ - discrete_resource_spec?: { - kind?: string; - value?: number | string; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }>; - devices?: Array<{ - capabilities?: string[]; - count?: string | number; - device_ids?: string[]; - driver?: string; - options?: ListOrDict; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }>; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }; - restart_policy?: { - condition?: string; - delay?: string; - max_attempts?: number | string; - window?: string; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }; - placement?: { - constraints?: string[]; - preferences?: Array<{ - spread?: string; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }>; - max_replicas_per_node?: number | string; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; -} - -type Command = string | string[] | null; -type EnvFile = - | string - | Array< - string | { path: string; format?: string; required?: boolean | string } - >; -type StringOrList = string | string[]; -type ListOrDict = - | { [key: string]: string | number | boolean | null } - | string[]; -type ExtraHosts = { [host: string]: string | string[] } | string[]; -interface BlkioLimit { - path: string; - rate: number | string; -} -interface BlkioWeight { - path: string; - weight: number | string; -} -type ServiceConfigOrSecret = - | string - | { - source: string; - target?: string; - uid?: string; - gid?: string; - mode?: number | string; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }; -type Ulimits = { - [key: string]: - | number - | string - | { hard: number | string; soft: number | string }; -}; - -interface ServiceHook { - command?: Command; - user?: string; - privileged?: boolean | string; - working_dir?: string; - environment?: ListOrDict; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; -} - -interface Network { - name?: string; - driver?: string; - driver_opts?: { [key: string]: string | number }; - ipam?: { - driver?: string; - config?: Array<{ - subnet?: string; - ip_range?: string; - gateway?: string; - aux_addresses?: { [key: string]: string }; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }>; - options?: { [key: string]: string }; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - external?: boolean | string | { name?: string; [key: `x-${string}`]: any }; - internal?: boolean | string; - enable_ipv4?: boolean | string; - enable_ipv6?: boolean | string; - attachable?: boolean | string; - labels?: ListOrDict; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; -} - -interface Volume { - name?: string; - driver?: string; - driver_opts?: { [key: string]: string | number }; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - external?: boolean | string | { name?: string; [key: `x-${string}`]: any }; - labels?: ListOrDict; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; -} - -interface Secret { - name?: string; - environment?: string; - file?: string; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - external?: boolean | string | { name?: string; [key: string]: any }; - labels?: ListOrDict; - driver?: string; - driver_opts?: { [key: string]: string | number }; - template_driver?: string; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; -} - -interface Config { - name?: string; - content?: string; - environment?: string; - file?: string; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - external?: boolean | string | { name?: string; [key: string]: any }; - labels?: ListOrDict; - template_driver?: string; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; -} diff --git a/src/typings/docker.ts b/src/typings/docker.ts deleted file mode 100644 index 7e3b01f..0000000 --- a/src/typings/docker.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { ContainerStats } from "dockerode"; -import type Docker from "dockerode"; - -interface DockerHost { - name: string; - hostAddress: string; - secure: boolean; - id: number; -} - -interface ContainerInfo { - id: string; - hostId: string; - name: string; - image: string; - status: string; - state: string; - cpuUsage: number; - memoryUsage: number; - stats?: ContainerStats; - info?: Docker.ContainerInfo; -} - -interface HostStats { - hostName: string; - hostId: number; - dockerVersion: string; - apiVersion: string; - os: string; - architecture: string; - totalMemory: number; - totalCPU: number; - labels: string[]; - containers: number; - containersRunning: number; - containersStopped: number; - containersPaused: number; - images: number; -} - -export type { HostStats, ContainerInfo, DockerHost }; diff --git a/src/typings/dockerode.ts b/src/typings/dockerode.ts deleted file mode 100644 index e1268ad..0000000 --- a/src/typings/dockerode.ts +++ /dev/null @@ -1,162 +0,0 @@ -interface DockerInfo { - ID: string; - Containers: number; - ContainersRunning: number; - ContainersPaused: number; - ContainersStopped: number; - Images: number; - Driver: string; - DriverStatus: [string, string][]; - DockerRootDir: string; - SystemStatus: [string, string][]; - Plugins: { - Volume: string[]; - Network: string[]; - Authorization: string[]; - Log: string[]; - }; - MemoryLimit: boolean; - SwapLimit: boolean; - KernelMemory: boolean; - CpuCfsPeriod: boolean; - CpuCfsQuota: boolean; - CPUShares: boolean; - CPUSet: boolean; - OomKillDisable: boolean; - IPv4Forwarding: boolean; - BridgeNfIptables: boolean; - BridgeNfIp6tables: boolean; - Debug: boolean; - NFd: number; - NGoroutines: number; - SystemTime: string; - LoggingDriver: string; - CgroupDriver: string; - NEventsListener: number; - KernelVersion: string; - OperatingSystem: string; - OSType: string; - Architecture: string; - NCPU: number; - MemTotal: number; - IndexServerAddress: string; - RegistryConfig: { - AllowNondistributableArtifactsCIDRs: string[]; - AllowNondistributableArtifactsHostnames: string[]; - InsecureRegistryCIDRs: string[]; - IndexConfigs: Record< - string, - { - Name: string; - Mirrors: string[]; - Secure: boolean; - Official: boolean; - } - >; - Mirrors: string[]; - }; - GenericResources: Array< - | { DiscreteResourceSpec: { Kind: string; Value: number } } - | { NamedResourceSpec: { Kind: string; Value: string } } - >; - HttpProxy: string; - HttpsProxy: string; - NoProxy: string; - Name: string; - Labels: string[]; - ExperimentalBuild: boolean; - ServerVersion: string; - ClusterStore: string; - ClusterAdvertise: string; - Runtimes: Record< - string, - { - path: string; - runtimeArgs?: string[]; - } - >; - DefaultRuntime: string; - Swarm: { - NodeID: string; - NodeAddr: string; - LocalNodeState: string; - ControlAvailable: boolean; - Error: string; - RemoteManagers: Array<{ - NodeID: string; - Addr: string; - }>; - Nodes: number; - Managers: number; - Cluster: { - ID: string; - Version: { - Index: number; - }; - CreatedAt: string; - UpdatedAt: string; - Spec: { - Name: string; - Labels: Record; - Orchestration: { - TaskHistoryRetentionLimit: number; - }; - Raft: { - SnapshotInterval: number; - KeepOldSnapshots: number; - LogEntriesForSlowFollowers: number; - ElectionTick: number; - HeartbeatTick: number; - }; - Dispatcher: { - HeartbeatPeriod: number; - }; - CAConfig: { - NodeCertExpiry: number; - ExternalCAs: Array<{ - Protocol: string; - URL: string; - Options: Record; - CACert: string; - }>; - SigningCACert: string; - SigningCAKey: string; - ForceRotate: number; - }; - EncryptionConfig: { - AutoLockManagers: boolean; - }; - TaskDefaults: { - LogDriver: { - Name: string; - Options: Record; - }; - }; - }; - TLSInfo: { - TrustRoot: string; - CertIssuerSubject: string; - CertIssuerPublicKey: string; - }; - RootRotationInProgress: boolean; - }; - }; - LiveRestoreEnabled: boolean; - Isolation: string; - InitBinary: string; - ContainerdCommit: { - ID: string; - Expected: string; - }; - RuncCommit: { - ID: string; - Expected: string; - }; - InitCommit: { - ID: string; - Expected: string; - }; - SecurityOptions: string[]; -} - -export type { DockerInfo }; diff --git a/src/typings/elysiajs.ts b/src/typings/elysiajs.ts deleted file mode 100644 index a68bb8c..0000000 --- a/src/typings/elysiajs.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { StatusMap } from "elysia"; -import type { ElysiaCookie } from "elysia/dist/cookies"; -import type { HTTPHeaders } from "elysia/dist/types"; - -interface set { - headers: HTTPHeaders; - status?: number | keyof StatusMap; - redirect?: string; - cookie?: Record; -} - -export type { set }; diff --git a/src/typings/misc.ts b/src/typings/misc.ts deleted file mode 100644 index c8bdc46..0000000 --- a/src/typings/misc.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type BackupInfo = { - filename: string; - date: Date; - backupNum: number; -}; diff --git a/src/typings/plugin.ts b/src/typings/plugin.ts deleted file mode 100644 index c5c3cc0..0000000 --- a/src/typings/plugin.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { ContainerInfo } from "~/typings/docker"; - -interface Plugin { - name: string; - - // Container lifecycle hooks - onContainerStart?: (containerInfo: ContainerInfo) => void; - onContainerStop?: (containerInfo: ContainerInfo) => void; - onContainerExit?: (containerInfo: ContainerInfo) => void; - onContainerCreate?: (containerInfo: ContainerInfo) => void; - onContainerKill?: (ContainerInfo: ContainerInfo) => void; - handleContainerDie?: (ContainerInfo: ContainerInfo) => void; - onContainerDestroy?: (containerInfo: ContainerInfo) => void; - onContainerPause?: (containerInfo: ContainerInfo) => void; - onContainerUnpause?: (containerInfo: ContainerInfo) => void; - onContainerRestart?: (containerInfo: ContainerInfo) => void; - onContainerUpdate?: (containerInfo: ContainerInfo) => void; - onContainerRename?: (containerInfo: ContainerInfo) => void; - onContainerHealthStatus?: (containerInfo: ContainerInfo) => void; - - // Host lifecycle hooks - onHostUnreachable?: (host: string, err: string) => void; - onHostReachableAgain?: (host: string) => void; -} - -export type { Plugin }; diff --git a/src/typings/websocket.ts b/src/typings/websocket.ts deleted file mode 100644 index e7ed96d..0000000 --- a/src/typings/websocket.ts +++ /dev/null @@ -1,15 +0,0 @@ -interface stackSocketMessage { - message?: string; - type?: "stack-progress" | "stack-error" | "stack-status" | "stack-removed"; - data?: stackSocketData; -} - -interface stackSocketData { - stack_id: number; - message: string; - action?: string; - status?: string; - timestamp?: string; -} - -export type { stackSocketMessage }; From 59e9ee1ce7b8e43ad4fd9fc05cfb82ccd3544a4b Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 30 Apr 2025 17:04:32 +0000 Subject: [PATCH 286/324] Update dependency graphs --- dependency-graph.mmd | 379 +++++++-------- dependency-graph.svg | 1101 +++++++++++++++++++++--------------------- 2 files changed, 740 insertions(+), 740 deletions(-) diff --git a/dependency-graph.mmd b/dependency-graph.mmd index bb9e74a..dcab557 100644 --- a/dependency-graph.mmd +++ b/dependency-graph.mmd @@ -8,224 +8,225 @@ flowchart LR subgraph 0["src"] 1["index.ts"] -subgraph 2["routes"] -3["live-stacks.ts"] -S["live-logs.ts"] -1G["api-config.ts"] -1I["docker-manager.ts"] -1J["docker-stats.ts"] -1K["docker-websocket.ts"] -1M["logs.ts"] -1N["stacks.ts"] -1Q["utils.ts"] +subgraph 5["routes"] +6["live-stacks.ts"] +U["live-logs.ts"] +1H["api-config.ts"] +1J["docker-manager.ts"] +1K["docker-stats.ts"] +1L["docker-websocket.ts"] +1N["logs.ts"] +1O["stacks.ts"] +1R["utils.ts"] end -subgraph 4["core"] -subgraph 5["utils"] -6["logger.ts"] -Q["helpers.ts"] -14["calculations.ts"] -18["change-me-checker.ts"] -1A["package-json.ts"] -1C["swagger-readme.ts"] -1H["response-handler.ts"] +subgraph 8["core"] +subgraph 9["utils"] +A["logger.ts"] +T["helpers.ts"] +15["calculations.ts"] +19["change-me-checker.ts"] +1B["package-json.ts"] +1D["swagger-readme.ts"] +1I["response-handler.ts"] end -subgraph 8["database"] -9["_dbState.ts"] -A["index.ts"] -B["backup.ts"] -D["database.ts"] -F["helper.ts"] -I["config.ts"] -J["containerStats.ts"] -K["dockerHosts.ts"] -M["hostStats.ts"] -N["logs.ts"] -P["stacks.ts"] +subgraph C["database"] +D["_dbState.ts"] +E["index.ts"] +F["backup.ts"] +I["database.ts"] +K["helper.ts"] +L["config.ts"] +M["containerStats.ts"] +N["dockerHosts.ts"] +P["hostStats.ts"] +Q["logs.ts"] +R["stacks.ts"] end -subgraph U["docker"] -V["monitor.ts"] -11["client.ts"] -12["scheduler.ts"] -13["store-container-stats.ts"] -15["store-host-stats.ts"] +subgraph V["docker"] +W["monitor.ts"] +12["client.ts"] +13["scheduler.ts"] +14["store-container-stats.ts"] +16["store-host-stats.ts"] end -subgraph X["plugins"] -Y["plugin-manager.ts"] -17["loader.ts"] +subgraph Y["plugins"] +Z["plugin-manager.ts"] +18["loader.ts"] end -subgraph 1O["stacks"] -1P["controller.ts"] +subgraph 1P["stacks"] +1Q["controller.ts"] end end -subgraph G["typings"] -H["misc.ts"] -L["docker.ts"] -O["database.ts"] -R["docker-compose.ts"] -T["websocket.ts"] -10["plugin.ts"] -16["dockerode.ts"] -1F["elysiajs.ts"] +subgraph 1E["middleware"] +1F["auth.ts"] end -subgraph 1D["middleware"] -1E["auth.ts"] end +subgraph 2["~"] +subgraph 3["typings"] +4["database"] +7["websocket"] +G["misc"] +O["docker"] +S["docker-compose"] +10["plugin"] +17["dockerode"] +1G["elysiajs"] end -7["path"] -subgraph C["fs"] -19["promises"] end -E["bun:sqlite"] -W["bun"] -Z["events"] -1B["package.json"] -1L["stream"] -1-->3 -1-->A -1-->V -1-->12 -1-->17 +B["path"] +subgraph H["fs"] +1A["promises"] +end +J["bun:sqlite"] +X["bun"] +11["events"] +1C["package.json"] +1M["stream"] 1-->6 -1-->1A -1-->1C -1-->1E -1-->1G -1-->1I +1-->E +1-->W +1-->13 +1-->18 +1-->A +1-->1B +1-->1D +1-->1F +1-->1H 1-->1J 1-->1K -1-->S -1-->1M +1-->1L +1-->U 1-->1N -1-->1Q -1-->O -3-->6 -3-->T -6-->9 +1-->1O +1-->1R +1-->4 6-->A -6-->S -6-->O 6-->7 -A-->B -A-->I -A-->J A-->D -A-->K -A-->M -A-->N -A-->P -B-->9 -B-->D -B-->F -B-->6 -B-->H -B-->C -D-->E -D-->C -F-->9 -F-->6 -I-->D -I-->F -J-->D -J-->F +A-->E +A-->U +A-->4 +A-->B +E-->F +E-->L +E-->M +E-->I +E-->N +E-->P +E-->Q +E-->R +F-->D +F-->I +F-->K +F-->A +F-->G +F-->H +I-->J +I-->H K-->D -K-->F -K-->L -M-->D -M-->F -M-->L -N-->D -N-->F +K-->A +L-->I +L-->K +M-->I +M-->K +N-->I +N-->K N-->O -P-->Q -P-->D -P-->F +P-->I +P-->K P-->O -P-->R -Q-->6 -S-->6 -S-->O -V-->Y -V-->A -V-->11 -V-->6 -V-->L -V-->W -Y-->6 -Y-->L -Y-->10 -Y-->Z -10-->L -11-->6 -11-->L +Q-->I +Q-->K +Q-->4 +R-->T +R-->I +R-->K +R-->4 +R-->S +T-->A +U-->A +U-->4 +W-->Z +W-->E +W-->12 +W-->A +W-->O +W-->X +Z-->A +Z-->O +Z-->10 +Z-->11 12-->A -12-->13 -12-->15 -12-->6 12-->O -13-->6 -13-->A -13-->11 +13-->E 13-->14 -15-->A -15-->11 -15-->Q -15-->6 -15-->L -15-->16 -17-->18 -17-->6 -17-->Y -17-->C -17-->7 -18-->6 +13-->16 +13-->A +13-->4 +14-->A +14-->E +14-->12 +14-->15 +16-->E +16-->12 +16-->T +16-->A +16-->O +16-->17 18-->19 -1A-->1B -1E-->A -1E-->6 -1E-->O -1E-->1F -1G-->A -1G-->B -1G-->Y -1G-->6 -1G-->1A -1G-->1H -1G-->1E -1G-->O -1G-->C -1H-->6 +18-->A +18-->Z +18-->H +18-->B +19-->A +19-->1A +1B-->1C +1F-->E +1F-->A +1F-->4 +1F-->1G +1H-->E +1H-->F +1H-->Z +1H-->A +1H-->1B +1H-->1I 1H-->1F +1H-->4 +1H-->H 1I-->A -1I-->6 -1I-->1H -1I-->L +1I-->1G +1J-->E 1J-->A -1J-->11 -1J-->14 -1J-->Q -1J-->6 -1J-->1H -1J-->L -1J-->16 +1J-->1I +1J-->O +1K-->E +1K-->12 +1K-->15 +1K-->T 1K-->A -1K-->11 -1K-->14 -1K-->6 -1K-->1H -1K-->1L -1M-->A -1M-->6 +1K-->1I +1K-->O +1K-->17 +1L-->E +1L-->12 +1L-->15 +1L-->A +1L-->1I +1L-->1M +1N-->E 1N-->A -1N-->1P -1N-->6 -1N-->1H -1P-->Q -1P-->A -1P-->6 -1P-->3 -1P-->O -1P-->R -1P-->19 +1O-->E +1O-->1Q +1O-->A +1O-->1I +1Q-->T +1Q-->E +1Q-->A +1Q-->6 +1Q-->4 +1Q-->S 1Q-->1A -1Q-->1H +1R-->1B +1R-->1I diff --git a/dependency-graph.svg b/dependency-graph.svg index 3aa239a..7a12bb4 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -4,72 +4,77 @@ - - + + dependency-cruiser output - + cluster_fs - -fs + +fs cluster_src - -src + +src cluster_src/core - -core + +core cluster_src/core/database - -database + +database cluster_src/core/docker - -docker + +docker cluster_src/core/plugins - -plugins + +plugins cluster_src/core/stacks - -stacks + +stacks cluster_src/core/utils - -utils + +utils cluster_src/middleware - -middleware + +middleware cluster_src/routes - -routes + +routes -cluster_src/typings - -typings +cluster_~ + +~ + + +cluster_~/typings + +typings bun - -bun + +bun @@ -77,8 +82,8 @@ bun:sqlite - -bun:sqlite + +bun:sqlite @@ -86,8 +91,8 @@ events - -events + +events @@ -95,8 +100,8 @@ fs - -fs + +fs @@ -104,8 +109,8 @@ fs/promises - -promises + +promises @@ -113,8 +118,8 @@ package.json - -package.json + +package.json @@ -122,8 +127,8 @@ path - -path + +path @@ -131,8 +136,8 @@ src/core/database/_dbState.ts - -_dbState.ts + +_dbState.ts @@ -140,902 +145,896 @@ src/core/database/backup.ts - -backup.ts + +backup.ts src/core/database/backup.ts->fs - - + + src/core/database/backup.ts->src/core/database/_dbState.ts - - + + src/core/database/database.ts - -database.ts + +database.ts src/core/database/backup.ts->src/core/database/database.ts - - + + src/core/database/helper.ts - -helper.ts + +helper.ts src/core/database/backup.ts->src/core/database/helper.ts - - - - + + + + src/core/utils/logger.ts - -logger.ts + +logger.ts src/core/database/backup.ts->src/core/utils/logger.ts - - - - + + + + - + -src/typings/misc.ts - - -misc.ts +~/typings/misc + + +misc - + -src/core/database/backup.ts->src/typings/misc.ts - - +src/core/database/backup.ts->~/typings/misc + + src/core/database/database.ts->bun:sqlite - - + + src/core/database/database.ts->fs - - + + src/core/database/helper.ts->src/core/database/_dbState.ts - - + + src/core/database/helper.ts->src/core/utils/logger.ts - - - - + + + + src/core/utils/logger.ts->path - - + + src/core/utils/logger.ts->src/core/database/_dbState.ts - - + + src/core/database/index.ts - -index.ts + +index.ts src/core/utils/logger.ts->src/core/database/index.ts - - - - + + + + - + -src/typings/database.ts - - -database.ts +~/typings/database + + +database - + -src/core/utils/logger.ts->src/typings/database.ts - - +src/core/utils/logger.ts->~/typings/database + + src/routes/live-logs.ts - -live-logs.ts + +live-logs.ts src/core/utils/logger.ts->src/routes/live-logs.ts - - - - + + + + src/core/database/config.ts - -config.ts + +config.ts src/core/database/config.ts->src/core/database/database.ts - - + + src/core/database/config.ts->src/core/database/helper.ts - - - - + + + + src/core/database/containerStats.ts - -containerStats.ts + +containerStats.ts src/core/database/containerStats.ts->src/core/database/database.ts - - + + src/core/database/containerStats.ts->src/core/database/helper.ts - - - - + + + + src/core/database/dockerHosts.ts - -dockerHosts.ts + +dockerHosts.ts src/core/database/dockerHosts.ts->src/core/database/database.ts - - + + src/core/database/dockerHosts.ts->src/core/database/helper.ts - - - - + + + + - + -src/typings/docker.ts - - -docker.ts +~/typings/docker + + +docker - + -src/core/database/dockerHosts.ts->src/typings/docker.ts - - +src/core/database/dockerHosts.ts->~/typings/docker + + src/core/database/hostStats.ts - -hostStats.ts + +hostStats.ts src/core/database/hostStats.ts->src/core/database/database.ts - - + + src/core/database/hostStats.ts->src/core/database/helper.ts - - - - + + + + - + -src/core/database/hostStats.ts->src/typings/docker.ts - - +src/core/database/hostStats.ts->~/typings/docker + + src/core/database/index.ts->src/core/database/backup.ts - - - - + + + + src/core/database/index.ts->src/core/database/database.ts - - + + src/core/database/index.ts->src/core/database/config.ts - - - - + + + + src/core/database/index.ts->src/core/database/containerStats.ts - - - - + + + + src/core/database/index.ts->src/core/database/dockerHosts.ts - - - - + + + + src/core/database/index.ts->src/core/database/hostStats.ts - - - - + + + + src/core/database/logs.ts - -logs.ts + +logs.ts src/core/database/index.ts->src/core/database/logs.ts - - - - + + + + src/core/database/stacks.ts - -stacks.ts + +stacks.ts src/core/database/index.ts->src/core/database/stacks.ts - - - - + + + + src/core/database/logs.ts->src/core/database/database.ts - - + + src/core/database/logs.ts->src/core/database/helper.ts - - - - + + + + - + -src/core/database/logs.ts->src/typings/database.ts - - +src/core/database/logs.ts->~/typings/database + + src/core/database/stacks.ts->src/core/database/database.ts - - + + src/core/database/stacks.ts->src/core/database/helper.ts - - - - + + + + - + -src/core/database/stacks.ts->src/typings/database.ts - - +src/core/database/stacks.ts->~/typings/database + + src/core/utils/helpers.ts - -helpers.ts + +helpers.ts src/core/database/stacks.ts->src/core/utils/helpers.ts - - - - + + + + - + -src/typings/docker-compose.ts - - -docker-compose.ts +~/typings/docker-compose + + +docker-compose - + -src/core/database/stacks.ts->src/typings/docker-compose.ts - - +src/core/database/stacks.ts->~/typings/docker-compose + + src/core/utils/helpers.ts->src/core/utils/logger.ts - - - - + + + + src/core/docker/client.ts - -client.ts + +client.ts src/core/docker/client.ts->src/core/utils/logger.ts - - + + - + -src/core/docker/client.ts->src/typings/docker.ts - - +src/core/docker/client.ts->~/typings/docker + + src/core/docker/monitor.ts - -monitor.ts + +monitor.ts src/core/docker/monitor.ts->bun - - + + src/core/docker/monitor.ts->src/core/utils/logger.ts - - + + - + -src/core/docker/monitor.ts->src/typings/docker.ts - - +src/core/docker/monitor.ts->~/typings/docker + + src/core/docker/monitor.ts->src/core/database/index.ts - - + + src/core/docker/monitor.ts->src/core/docker/client.ts - - + + src/core/plugins/plugin-manager.ts - -plugin-manager.ts + +plugin-manager.ts src/core/docker/monitor.ts->src/core/plugins/plugin-manager.ts - - + + src/core/plugins/plugin-manager.ts->events - - + + src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts - - + + - + -src/core/plugins/plugin-manager.ts->src/typings/docker.ts - - +src/core/plugins/plugin-manager.ts->~/typings/docker + + - + -src/typings/plugin.ts - - -plugin.ts +~/typings/plugin + + +plugin - + -src/core/plugins/plugin-manager.ts->src/typings/plugin.ts - - +src/core/plugins/plugin-manager.ts->~/typings/plugin + + src/core/docker/scheduler.ts - -scheduler.ts + +scheduler.ts src/core/docker/scheduler.ts->src/core/utils/logger.ts - - + + src/core/docker/scheduler.ts->src/core/database/index.ts - - + + - + -src/core/docker/scheduler.ts->src/typings/database.ts - - +src/core/docker/scheduler.ts->~/typings/database + + src/core/docker/store-container-stats.ts - -store-container-stats.ts + +store-container-stats.ts src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts - - + + src/core/docker/store-host-stats.ts - -store-host-stats.ts + +store-host-stats.ts src/core/docker/scheduler.ts->src/core/docker/store-host-stats.ts - - + + src/core/docker/store-container-stats.ts->src/core/utils/logger.ts - - + + src/core/docker/store-container-stats.ts->src/core/database/index.ts - - + + src/core/docker/store-container-stats.ts->src/core/docker/client.ts - - + + src/core/utils/calculations.ts - -calculations.ts + +calculations.ts src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts - - + + src/core/docker/store-host-stats.ts->src/core/utils/logger.ts - - + + - + -src/core/docker/store-host-stats.ts->src/typings/docker.ts - - +src/core/docker/store-host-stats.ts->~/typings/docker + + src/core/docker/store-host-stats.ts->src/core/database/index.ts - - + + src/core/docker/store-host-stats.ts->src/core/utils/helpers.ts - - + + src/core/docker/store-host-stats.ts->src/core/docker/client.ts - - + + - + -src/typings/dockerode.ts - - -dockerode.ts +~/typings/dockerode + + +dockerode - + -src/core/docker/store-host-stats.ts->src/typings/dockerode.ts - - +src/core/docker/store-host-stats.ts->~/typings/dockerode + + src/core/plugins/loader.ts - -loader.ts + +loader.ts src/core/plugins/loader.ts->fs - - + + src/core/plugins/loader.ts->path - - + + src/core/plugins/loader.ts->src/core/utils/logger.ts - - + + src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts - - + + src/core/utils/change-me-checker.ts - -change-me-checker.ts + +change-me-checker.ts src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts - - + + src/core/utils/change-me-checker.ts->fs/promises - - + + src/core/utils/change-me-checker.ts->src/core/utils/logger.ts - - - - - -src/typings/plugin.ts->src/typings/docker.ts - - + + src/core/stacks/controller.ts - -controller.ts + +controller.ts src/core/stacks/controller.ts->fs/promises - - + + src/core/stacks/controller.ts->src/core/utils/logger.ts - - + + src/core/stacks/controller.ts->src/core/database/index.ts - - + + - + -src/core/stacks/controller.ts->src/typings/database.ts - - +src/core/stacks/controller.ts->~/typings/database + + src/core/stacks/controller.ts->src/core/utils/helpers.ts - - + + - + -src/core/stacks/controller.ts->src/typings/docker-compose.ts - - +src/core/stacks/controller.ts->~/typings/docker-compose + + src/routes/live-stacks.ts - -live-stacks.ts + +live-stacks.ts src/core/stacks/controller.ts->src/routes/live-stacks.ts - - + + src/routes/live-stacks.ts->src/core/utils/logger.ts - - + + - + -src/typings/websocket.ts - - -websocket.ts +~/typings/websocket + + +websocket - + -src/routes/live-stacks.ts->src/typings/websocket.ts - - +src/routes/live-stacks.ts->~/typings/websocket + + src/routes/live-logs.ts->src/core/utils/logger.ts - - - - + + + + - + -src/routes/live-logs.ts->src/typings/database.ts - - +src/routes/live-logs.ts->~/typings/database + + src/core/utils/package-json.ts - -package-json.ts + +package-json.ts src/core/utils/package-json.ts->package.json - - + + src/core/utils/response-handler.ts - -response-handler.ts + +response-handler.ts src/core/utils/response-handler.ts->src/core/utils/logger.ts - - + + - + -src/typings/elysiajs.ts - - -elysiajs.ts +~/typings/elysiajs + + +elysiajs - + -src/core/utils/response-handler.ts->src/typings/elysiajs.ts - - +src/core/utils/response-handler.ts->~/typings/elysiajs + + src/core/utils/swagger-readme.ts - -swagger-readme.ts + +swagger-readme.ts @@ -1043,433 +1042,433 @@ src/index.ts - -index.ts + +index.ts src/index.ts->src/core/utils/logger.ts - - + + src/index.ts->src/core/database/index.ts - - + + - + -src/index.ts->src/typings/database.ts - - +src/index.ts->~/typings/database + + src/index.ts->src/core/docker/monitor.ts - - + + src/index.ts->src/core/docker/scheduler.ts - - + + src/index.ts->src/core/plugins/loader.ts - - + + src/index.ts->src/routes/live-stacks.ts - - + + src/index.ts->src/routes/live-logs.ts - - + + src/index.ts->src/core/utils/package-json.ts - - + + src/index.ts->src/core/utils/swagger-readme.ts - - + + src/middleware/auth.ts - -auth.ts + +auth.ts src/index.ts->src/middleware/auth.ts - - + + src/routes/api-config.ts - -api-config.ts + +api-config.ts src/index.ts->src/routes/api-config.ts - - + + src/routes/docker-manager.ts - -docker-manager.ts + +docker-manager.ts src/index.ts->src/routes/docker-manager.ts - - + + src/routes/docker-stats.ts - -docker-stats.ts + +docker-stats.ts src/index.ts->src/routes/docker-stats.ts - - + + src/routes/docker-websocket.ts - -docker-websocket.ts + +docker-websocket.ts src/index.ts->src/routes/docker-websocket.ts - - + + src/routes/logs.ts - -logs.ts + +logs.ts src/index.ts->src/routes/logs.ts - - + + src/routes/stacks.ts - -stacks.ts + +stacks.ts src/index.ts->src/routes/stacks.ts - - + + src/routes/utils.ts - -utils.ts + +utils.ts src/index.ts->src/routes/utils.ts - - + + src/middleware/auth.ts->src/core/utils/logger.ts - - + + src/middleware/auth.ts->src/core/database/index.ts - - + + - + -src/middleware/auth.ts->src/typings/database.ts - - +src/middleware/auth.ts->~/typings/database + + - + -src/middleware/auth.ts->src/typings/elysiajs.ts - - +src/middleware/auth.ts->~/typings/elysiajs + + src/routes/api-config.ts->fs - - + + src/routes/api-config.ts->src/core/database/backup.ts - - + + src/routes/api-config.ts->src/core/utils/logger.ts - - + + src/routes/api-config.ts->src/core/database/index.ts - - + + - + -src/routes/api-config.ts->src/typings/database.ts - - +src/routes/api-config.ts->~/typings/database + + src/routes/api-config.ts->src/core/plugins/plugin-manager.ts - - + + src/routes/api-config.ts->src/core/utils/package-json.ts - - + + src/routes/api-config.ts->src/core/utils/response-handler.ts - - + + src/routes/api-config.ts->src/middleware/auth.ts - - + + src/routes/docker-manager.ts->src/core/utils/logger.ts - - + + - + -src/routes/docker-manager.ts->src/typings/docker.ts - - +src/routes/docker-manager.ts->~/typings/docker + + src/routes/docker-manager.ts->src/core/database/index.ts - - + + src/routes/docker-manager.ts->src/core/utils/response-handler.ts - - + + src/routes/docker-stats.ts->src/core/utils/logger.ts - - + + - + -src/routes/docker-stats.ts->src/typings/docker.ts - - +src/routes/docker-stats.ts->~/typings/docker + + src/routes/docker-stats.ts->src/core/database/index.ts - - + + src/routes/docker-stats.ts->src/core/utils/helpers.ts - - + + src/routes/docker-stats.ts->src/core/docker/client.ts - - + + src/routes/docker-stats.ts->src/core/utils/calculations.ts - - + + - + -src/routes/docker-stats.ts->src/typings/dockerode.ts - - +src/routes/docker-stats.ts->~/typings/dockerode + + src/routes/docker-stats.ts->src/core/utils/response-handler.ts - - + + src/routes/docker-websocket.ts->src/core/utils/logger.ts - - + + src/routes/docker-websocket.ts->src/core/database/index.ts - - + + src/routes/docker-websocket.ts->src/core/docker/client.ts - - + + src/routes/docker-websocket.ts->src/core/utils/calculations.ts - - + + src/routes/docker-websocket.ts->src/core/utils/response-handler.ts - - + + stream - -stream + +stream src/routes/docker-websocket.ts->stream - - + + src/routes/logs.ts->src/core/utils/logger.ts - - + + src/routes/logs.ts->src/core/database/index.ts - - + + src/routes/stacks.ts->src/core/utils/logger.ts - - + + src/routes/stacks.ts->src/core/database/index.ts - - + + src/routes/stacks.ts->src/core/stacks/controller.ts - - + + src/routes/stacks.ts->src/core/utils/response-handler.ts - - + + src/routes/utils.ts->src/core/utils/package-json.ts - - + + src/routes/utils.ts->src/core/utils/response-handler.ts - - + + From a24c66e4b141489b02e7654b63b2d22cd63a3b61 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 30 Apr 2025 19:07:25 +0200 Subject: [PATCH 287/324] Chroe: Update CodeQL --- package.json | 6 +- src/core/database/logs.ts | 14 +- src/core/utils/logger.ts | 13 +- src/index.ts | 8 +- src/routes/api-config.ts | 260 ++++++++++++++++++----------------- src/routes/docker-manager.ts | 120 ++++++++-------- src/routes/docker-stats.ts | 104 +++++++------- src/routes/live-logs.ts | 8 +- src/routes/logs.ts | 128 ++++++++--------- src/routes/stacks.ts | 252 ++++++++++++++++----------------- src/routes/utils.ts | 51 +++---- 11 files changed, 491 insertions(+), 473 deletions(-) diff --git a/package.json b/package.json index 6bf8d3b..a3e18f4 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,5 @@ "wrap-ansi": "^9.0.0" }, "module": "src/index.js", - "trustedDependencies": [ - "protobufjs" - ] -} \ No newline at end of file + "trustedDependencies": ["protobufjs"] +} diff --git a/src/core/database/logs.ts b/src/core/database/logs.ts index ad100c4..f6b9980 100644 --- a/src/core/database/logs.ts +++ b/src/core/database/logs.ts @@ -16,7 +16,7 @@ const stmt = { deleteByLevel: db.prepare("DELETE FROM backend_log_entries WHERE level = ?"), }; -function convertToLogMessage(row: any): log_message { +function convertToLogMessage(row: log_message): log_message { return { level: row.level, timestamp: row.timestamp, @@ -55,16 +55,16 @@ export function addLogEntry(data: log_message) { } export function getAllLogs(): log_message[] { - return executeDbOperation( - "Get All Logs", - () => stmt.selectAll.all().map(convertToLogMessage), + return executeDbOperation("Get All Logs", () => + stmt.selectAll.all().map((row) => convertToLogMessage(row as log_message)), ); } export function getLogsByLevel(level: string): log_message[] { - return executeDbOperation( - "Get Logs By Level", - () => stmt.selectByLevel.all(level).map(convertToLogMessage), + return executeDbOperation("Get Logs By Level", () => + stmt.selectByLevel + .all(level) + .map((row) => convertToLogMessage(row as log_message)), ); } diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts index 30e6045..e8b8404 100644 --- a/src/core/utils/logger.ts +++ b/src/core/utils/logger.ts @@ -69,7 +69,14 @@ const parseTimestamp = (timestamp: string): string => { const [day, month] = datePart.split("/"); const [hours, minutes, seconds] = timePart.split(":"); const year = new Date().getFullYear(); - const date = new Date(year, parseInt(month) - 1, parseInt(day), parseInt(hours), parseInt(minutes), parseInt(seconds)); + const date = new Date( + year, + parseInt(month) - 1, + parseInt(day), + parseInt(hours), + parseInt(minutes), + parseInt(seconds), + ); return date.toISOString(); }; @@ -77,7 +84,7 @@ const handleWebSocketLog = (log: log_message) => { try { logToClients({ ...log, - timestamp: parseTimestamp(log.timestamp) + timestamp: parseTimestamp(log.timestamp), }); } catch (error) { console.error( @@ -95,7 +102,7 @@ const handleDatabaseLog = (log: log_message): void => { try { dbFunctions.addLogEntry({ ...log, - timestamp: parseTimestamp(log.timestamp) + timestamp: parseTimestamp(log.timestamp), }); } catch (error) { console.error( diff --git a/src/index.ts b/src/index.ts index 062d8b8..badec71 100644 --- a/src/index.ts +++ b/src/index.ts @@ -76,7 +76,7 @@ export const DockStatAPI = new Elysia() { name: "Utils", description: "Various utilities which might be useful", - } + }, ], }, }), @@ -84,7 +84,11 @@ export const DockStatAPI = new Elysia() .onBeforeHandle(async (context) => { const { path, request, set } = context; - if (path === "/health" || path.startsWith("/swagger") || path.startsWith("/trpc")) { + if ( + path === "/health" || + path.startsWith("/swagger") || + path.startsWith("/trpc") + ) { logger.info(`Requested unguarded route: ${path}`); return; } diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index 354ffb0..5ccca3e 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -54,20 +54,20 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) properties: { fetching_interval: { type: "number", - example: 5 + example: 5, }, keep_data_for: { type: "number", - example: 7 + example: 7, }, api_key: { type: "string", - example: "hashed_api_key" - } - } - } - } - } + example: "hashed_api_key", + }, + }, + }, + }, + }, }, "400": { description: "Error retrieving configuration", @@ -78,14 +78,14 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) properties: { error: { type: "string", - example: "Error getting the DockStatAPI config" - } - } - } - } - } - } - } + example: "Error getting the DockStatAPI config", + }, + }, + }, + }, + }, + }, + }, }, }, ) @@ -119,21 +119,21 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) properties: { name: { type: "string", - example: "example-plugin" + example: "example-plugin", }, version: { type: "string", - example: "1.0.0" + example: "1.0.0", }, status: { type: "string", - example: "active" - } - } - } - } - } - } + example: "active", + }, + }, + }, + }, + }, + }, }, "400": { description: "Error retrieving plugins", @@ -144,14 +144,14 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) properties: { error: { type: "string", - example: "Error getting all registered plugins" - } - } - } - } - } - } - } + example: "Error getting all registered plugins", + }, + }, + }, + }, + }, + }, + }, }, }, ) @@ -190,12 +190,12 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) properties: { message: { type: "string", - example: "Updated DockStatAPI config" - } - } - } - } - } + example: "Updated DockStatAPI config", + }, + }, + }, + }, + }, }, "400": { description: "Error updating configuration", @@ -206,14 +206,14 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) properties: { error: { type: "string", - example: "Error updating the DockStatAPI config" - } - } - } - } - } - } - } + example: "Error updating the DockStatAPI config", + }, + }, + }, + }, + }, + }, + }, }, body: t.Object({ fetching_interval: t.Number(), @@ -261,53 +261,54 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) properties: { version: { type: "string", - example: "3.0.0" + example: "3.0.0", }, description: { type: "string", - example: "DockStatAPI is an API backend featuring plugins and more for DockStat" + example: + "DockStatAPI is an API backend featuring plugins and more for DockStat", }, license: { type: "string", - example: "CC BY-NC 4.0" + example: "CC BY-NC 4.0", }, authorName: { type: "string", - example: "ItsNik" + example: "ItsNik", }, authorEmail: { type: "string", - example: "info@itsnik.de" + example: "info@itsnik.de", }, authorWebsite: { type: "string", - example: "https://github.com/Its4Nik" + example: "https://github.com/Its4Nik", }, contributors: { type: "array", items: { - type: "string" + type: "string", }, - example: [] + example: [], }, dependencies: { type: "object", example: { "@elysiajs/server-timing": "^1.2.1", - "@elysiajs/static": "^1.2.0" - } + "@elysiajs/static": "^1.2.0", + }, }, devDependencies: { type: "object", example: { "@biomejs/biome": "1.9.4", - "@types/dockerode": "^3.3.38" - } - } - } - } - } - } + "@types/dockerode": "^3.3.38", + }, + }, + }, + }, + }, + }, }, "400": { description: "Error retrieving package information", @@ -318,14 +319,14 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) properties: { error: { type: "string", - example: "Error while reading package.json" - } - } - } - } - } - } - } + example: "Error while reading package.json", + }, + }, + }, + }, + }, + }, + }, }, }, ) @@ -353,12 +354,12 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) properties: { message: { type: "string", - example: "backup_2024-03-20_12-00-00.db.bak" - } - } - } - } - } + example: "backup_2024-03-20_12-00-00.db.bak", + }, + }, + }, + }, + }, }, "400": { description: "Error creating backup", @@ -369,14 +370,14 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) properties: { error: { type: "string", - example: "Error backing up" - } - } - } - } - } - } - } + example: "Error backing up", + }, + }, + }, + }, + }, + }, + }, }, }, ) @@ -415,15 +416,15 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) schema: { type: "array", items: { - type: "string" + type: "string", }, example: [ "backup_2024-03-20_12-00-00.db.bak", - "backup_2024-03-19_12-00-00.db.bak" - ] - } - } - } + "backup_2024-03-19_12-00-00.db.bak", + ], + }, + }, + }, }, "400": { description: "Error retrieving backup list", @@ -434,14 +435,14 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) properties: { error: { type: "string", - example: "Reading Backup directory" - } - } - } - } - } - } - } + example: "Reading Backup directory", + }, + }, + }, + }, + }, + }, + }, }, }, ) @@ -482,18 +483,19 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) schema: { type: "string", format: "binary", - example: "Binary backup file content" - } - } + example: "Binary backup file content", + }, + }, }, headers: { "Content-Disposition": { schema: { type: "string", - example: "attachment; filename=\"backup_2024-03-20_12-00-00.db.bak\"" - } - } - } + example: + 'attachment; filename="backup_2024-03-20_12-00-00.db.bak"', + }, + }, + }, }, "400": { description: "Error downloading backup", @@ -504,14 +506,14 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) properties: { error: { type: "string", - example: "Backup download failed" - } - } - } - } - } - } - } + example: "Backup download failed", + }, + }, + }, + }, + }, + }, + }, }, query: t.Object({ filename: t.Optional(t.String()), @@ -565,12 +567,12 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) properties: { message: { type: "string", - example: "Database restored successfully" - } - } - } - } - } + example: "Database restored successfully", + }, + }, + }, + }, + }, }, "400": { description: "Error restoring database", @@ -581,14 +583,14 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) properties: { error: { type: "string", - example: "Database restoration error" - } - } - } - } - } - } - } + example: "Database restoration error", + }, + }, + }, + }, + }, + }, + }, }, }, ); diff --git a/src/routes/docker-manager.ts b/src/routes/docker-manager.ts index 13f3312..68044cd 100644 --- a/src/routes/docker-manager.ts +++ b/src/routes/docker-manager.ts @@ -37,12 +37,12 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) properties: { message: { type: "string", - example: "Added docker host (Localhost)" - } - } - } - } - } + example: "Added docker host (Localhost)", + }, + }, + }, + }, + }, }, "400": { description: "Error adding Docker host", @@ -53,14 +53,14 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) properties: { error: { type: "string", - example: "Error adding docker Host" - } - } - } - } - } - } - } + example: "Error adding docker Host", + }, + }, + }, + }, + }, + }, + }, }, body: t.Object({ name: t.String(), @@ -100,12 +100,12 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) properties: { message: { type: "string", - example: "Updated docker host (1)" - } - } - } - } - } + example: "Updated docker host (1)", + }, + }, + }, + }, + }, }, "400": { description: "Error updating Docker host", @@ -116,14 +116,14 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) properties: { error: { type: "string", - example: "Failed to update host" - } - } - } - } - } - } - } + example: "Failed to update host", + }, + }, + }, + }, + }, + }, + }, }, body: t.Object({ id: t.Number(), @@ -167,25 +167,25 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) properties: { id: { type: "number", - example: 1 + example: 1, }, name: { type: "string", - example: "Localhost" + example: "Localhost", }, hostAddress: { type: "string", - example: "localhost:2375" + example: "localhost:2375", }, secure: { type: "boolean", - example: false - } - } - } - } - } - } + example: false, + }, + }, + }, + }, + }, + }, }, "400": { description: "Error retrieving Docker hosts", @@ -196,14 +196,14 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) properties: { error: { type: "string", - example: "Failed to retrieve hosts" - } - } - } - } - } - } - } + example: "Failed to retrieve hosts", + }, + }, + }, + }, + }, + }, + }, }, }, ) @@ -238,12 +238,12 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) properties: { message: { type: "string", - example: "Deleted docker host (1)" - } - } - } - } - } + example: "Deleted docker host (1)", + }, + }, + }, + }, + }, }, "400": { description: "Error deleting Docker host", @@ -254,14 +254,14 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) properties: { error: { type: "string", - example: "Failed to delete host" - } - } - } - } - } - } - } + example: "Failed to delete host", + }, + }, + }, + }, + }, + }, + }, }, params: t.Object({ id: t.Number(), diff --git a/src/routes/docker-stats.ts b/src/routes/docker-stats.ts index db10816..3781f26 100644 --- a/src/routes/docker-stats.ts +++ b/src/routes/docker-stats.ts @@ -123,43 +123,43 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) properties: { id: { type: "string", - example: "abc123def456" + example: "abc123def456", }, hostId: { type: "string", - example: "1" + example: "1", }, name: { type: "string", - example: "example-container" + example: "example-container", }, image: { type: "string", - example: "nginx:latest" + example: "nginx:latest", }, status: { type: "string", - example: "running" + example: "running", }, state: { type: "string", - example: "running" + example: "running", }, cpuUsage: { type: "number", - example: 0.5 + example: 0.5, }, memoryUsage: { type: "number", - example: 1024 - } - } - } - } - } - } - } - } + example: 1024, + }, + }, + }, + }, + }, + }, + }, + }, }, "400": { description: "Error retrieving container statistics", @@ -170,14 +170,14 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) properties: { error: { type: "string", - example: "Failed to retrieve containers" - } - } - } - } - } - } - } + example: "Failed to retrieve containers", + }, + }, + }, + }, + }, + }, + }, }, }, ) @@ -241,67 +241,67 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) properties: { hostId: { type: "number", - example: 1 + example: 1, }, hostName: { type: "string", - example: "Localhost" + example: "Localhost", }, dockerVersion: { type: "string", - example: "24.0.5" + example: "24.0.5", }, apiVersion: { type: "string", - example: "1.41" + example: "1.41", }, os: { type: "string", - example: "Linux" + example: "Linux", }, architecture: { type: "string", - example: "x86_64" + example: "x86_64", }, totalMemory: { type: "number", - example: 16777216 + example: 16777216, }, totalCPU: { type: "number", - example: 4 + example: 4, }, labels: { type: "array", items: { - type: "string" + type: "string", }, - example: ["environment=production"] + example: ["environment=production"], }, images: { type: "number", - example: 10 + example: 10, }, containers: { type: "number", - example: 5 + example: 5, }, containersPaused: { type: "number", - example: 0 + example: 0, }, containersRunning: { type: "number", - example: 4 + example: 4, }, containersStopped: { type: "number", - example: 1 - } - } - } - } - } + example: 1, + }, + }, + }, + }, + }, }, "400": { description: "Error retrieving host statistics", @@ -312,14 +312,14 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) properties: { error: { type: "string", - example: "Failed to retrieve host config" - } - } - } - } - } - } - } + example: "Failed to retrieve host config", + }, + }, + }, + }, + }, + }, + }, }, }, ); diff --git a/src/routes/live-logs.ts b/src/routes/live-logs.ts index 50e9ea8..cf6e4b5 100644 --- a/src/routes/live-logs.ts +++ b/src/routes/live-logs.ts @@ -11,7 +11,13 @@ const activeConnections = new Set>(); export const liveLogs = new Elysia({ prefix: "/logs" }).ws("/ws", { open(ws) { activeConnections.add(ws); - ws.send({ message: "Connection established", level: "info", timestamp: new Date().toISOString(), file: "live-logs.ts", line: 14 }); + ws.send({ + message: "Connection established", + level: "info", + timestamp: new Date().toISOString(), + file: "live-logs.ts", + line: 14, + }); logger.info(`New Logs WebSocket established (${ws.id})`); }, close(ws) { diff --git a/src/routes/logs.ts b/src/routes/logs.ts index 20b3be2..a4feb15 100644 --- a/src/routes/logs.ts +++ b/src/routes/logs.ts @@ -35,25 +35,25 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) properties: { id: { type: "number", - example: 1 + example: 1, }, level: { type: "string", - example: "info" + example: "info", }, message: { type: "string", - example: "Application started" + example: "Application started", }, timestamp: { type: "string", - example: "2024-03-20T12:00:00Z" - } - } - } - } - } - } + example: "2024-03-20T12:00:00Z", + }, + }, + }, + }, + }, + }, }, "500": { description: "Error retrieving logs", @@ -64,14 +64,14 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) properties: { error: { type: "string", - example: "Failed to retrieve logs" - } - } - } - } - } - } - } + example: "Failed to retrieve logs", + }, + }, + }, + }, + }, + }, + }, }, }, ) @@ -107,25 +107,25 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) properties: { id: { type: "number", - example: 1 + example: 1, }, level: { type: "string", - example: "info" + example: "info", }, message: { type: "string", - example: "Application started" + example: "Application started", }, timestamp: { type: "string", - example: "2024-03-20T12:00:00Z" - } - } - } - } - } - } + example: "2024-03-20T12:00:00Z", + }, + }, + }, + }, + }, + }, }, "500": { description: "Error retrieving logs", @@ -136,14 +136,14 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) properties: { error: { type: "string", - example: "Failed to retrieve logs" - } - } - } - } - } - } - } + example: "Failed to retrieve logs", + }, + }, + }, + }, + }, + }, + }, }, }, ) @@ -176,12 +176,12 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) properties: { success: { type: "boolean", - example: true - } - } - } - } - } + example: true, + }, + }, + }, + }, + }, }, "500": { description: "Error clearing logs", @@ -192,14 +192,14 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) properties: { error: { type: "string", - example: "Could not delete all logs" - } - } - } - } - } - } - } + example: "Could not delete all logs", + }, + }, + }, + }, + }, + }, + }, }, }, ) @@ -232,12 +232,12 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) properties: { success: { type: "boolean", - example: true - } - } - } - } - } + example: true, + }, + }, + }, + }, + }, }, "500": { description: "Error clearing logs", @@ -248,14 +248,14 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) properties: { error: { type: "string", - example: "Failed to retrieve logs" - } - } - } - } - } - } - } + example: "Failed to retrieve logs", + }, + }, + }, + }, + }, + }, + }, }, }, ); diff --git a/src/routes/stacks.ts b/src/routes/stacks.ts index 52eaf86..d5b2242 100644 --- a/src/routes/stacks.ts +++ b/src/routes/stacks.ts @@ -78,12 +78,12 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) properties: { message: { type: "string", - example: "Stack example-stack deployed successfully" - } - } - } - } - } + example: "Stack example-stack deployed successfully", + }, + }, + }, + }, + }, }, "400": { description: "Error deploying stack", @@ -94,14 +94,14 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) properties: { error: { type: "string", - example: "Error deploying stack" - } - } - } - } - } - } - } + example: "Error deploying stack", + }, + }, + }, + }, + }, + }, + }, }, body: t.Object({ compose_spec: t.Any(), @@ -149,12 +149,12 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) properties: { message: { type: "string", - example: "Stack 1 started successfully" - } - } - } - } - } + example: "Stack 1 started successfully", + }, + }, + }, + }, + }, }, "400": { description: "Error starting stack", @@ -165,14 +165,14 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) properties: { error: { type: "string", - example: "Error starting stack" - } - } - } - } - } - } - } + example: "Error starting stack", + }, + }, + }, + }, + }, + }, + }, }, body: t.Object({ stackId: t.Number(), @@ -213,12 +213,12 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) properties: { message: { type: "string", - example: "Stack 1 stopped successfully" - } - } - } - } - } + example: "Stack 1 stopped successfully", + }, + }, + }, + }, + }, }, "400": { description: "Error stopping stack", @@ -229,14 +229,14 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) properties: { error: { type: "string", - example: "Error stopping stack" - } - } - } - } - } - } - } + example: "Error stopping stack", + }, + }, + }, + }, + }, + }, + }, }, body: t.Object({ stackId: t.Number(), @@ -277,12 +277,12 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) properties: { message: { type: "string", - example: "Stack 1 restarted successfully" - } - } - } - } - } + example: "Stack 1 restarted successfully", + }, + }, + }, + }, + }, }, "400": { description: "Error restarting stack", @@ -293,14 +293,14 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) properties: { error: { type: "string", - example: "Error restarting stack" - } - } - } - } - } - } - } + example: "Error restarting stack", + }, + }, + }, + }, + }, + }, + }, }, body: t.Object({ stackId: t.Number(), @@ -341,12 +341,12 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) properties: { message: { type: "string", - example: "Images for stack 1 pulled successfully" - } - } - } - } - } + example: "Images for stack 1 pulled successfully", + }, + }, + }, + }, + }, }, "400": { description: "Error pulling images", @@ -357,14 +357,14 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) properties: { error: { type: "string", - example: "Error pulling images" - } - } - } - } - } - } - } + example: "Error pulling images", + }, + }, + }, + }, + }, + }, + }, }, body: t.Object({ stackId: t.Number(), @@ -416,18 +416,18 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) properties: { message: { type: "string", - example: "Stack 1 status retrieved successfully" + example: "Stack 1 status retrieved successfully", }, status: { type: "object", properties: { name: { type: "string", - example: "example-stack" + example: "example-stack", }, status: { type: "string", - example: "running" + example: "running", }, containers: { type: "array", @@ -436,21 +436,21 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) properties: { name: { type: "string", - example: "example-stack_web_1" + example: "example-stack_web_1", }, status: { type: "string", - example: "running" - } - } - } - } - } - } - } - } - } - } + example: "running", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, }, "400": { description: "Error getting stack status", @@ -461,14 +461,14 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) properties: { error: { type: "string", - example: "Error getting stack status" - } - } - } - } - } - } - } + example: "Error getting stack status", + }, + }, + }, + }, + }, + }, + }, }, query: t.Object({ stackId: t.Number(), @@ -505,29 +505,29 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) properties: { id: { type: "number", - example: 1 + example: 1, }, name: { type: "string", - example: "example-stack" + example: "example-stack", }, version: { type: "number", - example: 1 + example: 1, }, source: { type: "string", - example: "github.com/example/repo" + example: "github.com/example/repo", }, automatic_reboot_on_error: { type: "boolean", - example: true - } - } - } - } - } - } + example: true, + }, + }, + }, + }, + }, + }, }, "400": { description: "Error getting stacks", @@ -538,14 +538,14 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) properties: { error: { type: "string", - example: "Error getting stacks" - } - } - } - } - } - } - } + example: "Error getting stacks", + }, + }, + }, + }, + }, + }, + }, }, }, ) @@ -579,12 +579,12 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) properties: { message: { type: "string", - example: "Stack 1 deleted successfully" - } - } - } - } - } + example: "Stack 1 deleted successfully", + }, + }, + }, + }, + }, }, "400": { description: "Error deleting stack", @@ -595,14 +595,14 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) properties: { error: { type: "string", - example: "Error deleting stack" - } - } - } - } - } - } - } + example: "Error deleting stack", + }, + }, + }, + }, + }, + }, + }, }, body: t.Object({ stackId: t.Number(), diff --git a/src/routes/utils.ts b/src/routes/utils.ts index 0f0ca26..591efd5 100644 --- a/src/routes/utils.ts +++ b/src/routes/utils.ts @@ -52,53 +52,54 @@ export const utilRoutes = new Elysia({ prefix: "/utils" }).get( properties: { version: { type: "string", - example: "3.0.0" + example: "3.0.0", }, authorEmail: { type: "string", - example: "info@itsnik.de" + example: "info@itsnik.de", }, authorName: { type: "string", - example: "ItsNik" + example: "ItsNik", }, authorWebsite: { type: "string", - example: "https://github.com/Its4Nik" + example: "https://github.com/Its4Nik", }, contributors: { type: "array", items: { - type: "string" + type: "string", }, - example: [] + example: [], }, dependencies: { type: "object", example: { "@elysiajs/server-timing": "^1.2.1", - "@elysiajs/static": "^1.2.0" - } + "@elysiajs/static": "^1.2.0", + }, }, description: { type: "string", - example: "DockStatAPI is an API backend featuring plugins and more for DockStat" + example: + "DockStatAPI is an API backend featuring plugins and more for DockStat", }, devDependencies: { type: "object", example: { "@biomejs/biome": "1.9.4", - "@types/dockerode": "^3.3.38" - } + "@types/dockerode": "^3.3.38", + }, }, license: { type: "string", - example: "CC BY-NC 4.0" - } - } - } - } - } + example: "CC BY-NC 4.0", + }, + }, + }, + }, + }, }, "400": { description: "Error retrieving API information", @@ -109,14 +110,14 @@ export const utilRoutes = new Elysia({ prefix: "/utils" }).get( properties: { error: { type: "string", - example: "Error getting DockStatAPI information" - } - } - } - } - } - } - } + example: "Error getting DockStatAPI information", + }, + }, + }, + }, + }, + }, + }, }, }, ); From 911cb8582fe270fb64e68280d7bccbce234dd1e3 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Wed, 30 Apr 2025 17:08:49 +0000 Subject: [PATCH 288/324] CQL: Apply lint fixes [skip ci] --- src/core/utils/logger.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts index e8b8404..f9304ab 100644 --- a/src/core/utils/logger.ts +++ b/src/core/utils/logger.ts @@ -71,11 +71,11 @@ const parseTimestamp = (timestamp: string): string => { const year = new Date().getFullYear(); const date = new Date( year, - parseInt(month) - 1, - parseInt(day), - parseInt(hours), - parseInt(minutes), - parseInt(seconds), + Number.parseInt(month) - 1, + Number.parseInt(day), + Number.parseInt(hours), + Number.parseInt(minutes), + Number.parseInt(seconds), ); return date.toISOString(); }; From 00e825361b76aafc0bc8cfd824478bccf49b1afb Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 30 Apr 2025 22:07:30 +0200 Subject: [PATCH 289/324] Feat: Eden treaty --- src/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index badec71..7b4ffbc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,7 +33,7 @@ console.log(""); logger.info("Starting DockStatAPI"); -export const DockStatAPI = new Elysia() +const DockStatAPI = new Elysia() .use(staticPlugin()) .use(serverTiming()) .use( @@ -176,3 +176,5 @@ await startServer(); logger.info("Started server"); console.log("----- [ ############## ]"); + +export type DockStatAPI = typeof DockStatAPI; From d0489582c7dcccfab3af49a2672ee414f342a534 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 30 Apr 2025 22:08:53 +0200 Subject: [PATCH 290/324] Fix: Export for ci --- src/index.ts | 276 +++++++++++++++++++++++++-------------------------- 1 file changed, 138 insertions(+), 138 deletions(-) diff --git a/src/index.ts b/src/index.ts index 7b4ffbc..2780d10 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,9 +9,9 @@ import { setSchedules } from "~/core/docker/scheduler"; import { loadPlugins } from "~/core/plugins/loader"; import { logger } from "~/core/utils/logger"; import { - authorWebsite, - contributors, - license, + authorWebsite, + contributors, + license, } from "~/core/utils/package-json"; import { swaggerReadme } from "~/core/utils/swagger-readme"; @@ -33,143 +33,143 @@ console.log(""); logger.info("Starting DockStatAPI"); -const DockStatAPI = new Elysia() - .use(staticPlugin()) - .use(serverTiming()) - .use( - swagger({ - documentation: { - info: { - title: "DockStatAPI", - version: "3.0.0", - description: swaggerReadme, - }, - components: { - securitySchemes: { - apiKeyAuth: { - type: "apiKey", - name: "x-api-key", - in: "header", - description: "API key for authentication", - }, - }, - }, - security: [ - { - apiKeyAuth: [], - }, - ], - tags: [ - { - name: "Statistics", - description: - "All endpoints for fetching statistics of hosts / containers", - }, - { - name: "Management", - description: "Various endpoints for managing DockStatAPI", - }, - { - name: "Stacks", - description: "DockStat's Stack functionality", - }, - { - name: "Utils", - description: "Various utilities which might be useful", - }, - ], - }, - }), - ) - .onBeforeHandle(async (context) => { - const { path, request, set } = context; - - if ( - path === "/health" || - path.startsWith("/swagger") || - path.startsWith("/trpc") - ) { - logger.info(`Requested unguarded route: ${path}`); - return; - } - - const validation = await validateApiKey(request, set); - - if (validation.error) { - set.status = 400; - set.headers["Content-Type"] = "application/json"; - return { error: validation.error }; - } - }) - .use(dockerRoutes) - .use(dockerStatsRoutes) - .use(backendLogs) - .use(dockerWebsocketRoutes) - .use(apiConfigRoutes) - .use(utilRoutes) - .use(stackRoutes) - .use(utilRoutes) - .use(liveLogs) - .use(liveStacks) - .get("/health", () => ({ status: "healthy" }), { tags: ["Utils"] }) - .onError(({ code, set, path }) => { - if (code === "NOT_FOUND") { - logger.warn(`Unknown route (${path}), showing error page!`); - set.status = 404; - set.headers["Content-Type"] = "text/html"; - return Bun.file("public/404.html"); - } - }); +export const DockStatAPI = new Elysia() + .use(staticPlugin()) + .use(serverTiming()) + .use( + swagger({ + documentation: { + info: { + title: "DockStatAPI", + version: "3.0.0", + description: swaggerReadme, + }, + components: { + securitySchemes: { + apiKeyAuth: { + type: "apiKey", + name: "x-api-key", + in: "header", + description: "API key for authentication", + }, + }, + }, + security: [ + { + apiKeyAuth: [], + }, + ], + tags: [ + { + name: "Statistics", + description: + "All endpoints for fetching statistics of hosts / containers", + }, + { + name: "Management", + description: "Various endpoints for managing DockStatAPI", + }, + { + name: "Stacks", + description: "DockStat's Stack functionality", + }, + { + name: "Utils", + description: "Various utilities which might be useful", + }, + ], + }, + }) + ) + .onBeforeHandle(async (context) => { + const { path, request, set } = context; + + if ( + path === "/health" || + path.startsWith("/swagger") || + path.startsWith("/trpc") + ) { + logger.info(`Requested unguarded route: ${path}`); + return; + } + + const validation = await validateApiKey(request, set); + + if (validation.error) { + set.status = 400; + set.headers["Content-Type"] = "application/json"; + return { error: validation.error }; + } + }) + .use(dockerRoutes) + .use(dockerStatsRoutes) + .use(backendLogs) + .use(dockerWebsocketRoutes) + .use(apiConfigRoutes) + .use(utilRoutes) + .use(stackRoutes) + .use(utilRoutes) + .use(liveLogs) + .use(liveStacks) + .get("/health", () => ({ status: "healthy" }), { tags: ["Utils"] }) + .onError(({ code, set, path }) => { + if (code === "NOT_FOUND") { + logger.warn(`Unknown route (${path}), showing error page!`); + set.status = 404; + set.headers["Content-Type"] = "text/html"; + return Bun.file("public/404.html"); + } + }); async function startServer() { - try { - try { - await loadPlugins("./src/plugins"); - } catch (error) { - throw new Error(`Failed to load plugins: ${error}`); - } - - try { - await setSchedules(); - } catch (error) { - throw new Error(`Failed to set schedules: ${error}`); - } - - monitorDockerEvents().catch((error) => { - logger.error(`Monitoring Error: ${error}`); - }); - - const configData = dbFunctions.getConfig() as config[]; - const apiKey = configData[0].api_key; - - if (apiKey === "changeme") { - logger.warn( - "Default API Key of 'changeme' detected. Please change your API Key via the `/config/update` route!", - ); - } - - try { - DockStatAPI.listen( - process.env.DOCKSTATAPI_PORT || 3000, - ({ hostname, port }) => { - console.log("----- [ ############## ]"); - logger.info(`DockStatAPI is running at http://${hostname}:${port}`); - logger.info( - `Swagger API Documentation available at http://${hostname}:${port}/swagger`, - ); - logger.info(`License: ${license}`); - logger.info(`Author: ${authorWebsite}`); - logger.info(`Contributors: ${contributors}`); - }, - ); - } catch (error) { - logger.error("Failed to start server:", error); - process.exit(1); - } - } catch (error) { - logger.error("Error while starting server:", error); - process.exit(1); - } + try { + try { + await loadPlugins("./src/plugins"); + } catch (error) { + throw new Error(`Failed to load plugins: ${error}`); + } + + try { + await setSchedules(); + } catch (error) { + throw new Error(`Failed to set schedules: ${error}`); + } + + monitorDockerEvents().catch((error) => { + logger.error(`Monitoring Error: ${error}`); + }); + + const configData = dbFunctions.getConfig() as config[]; + const apiKey = configData[0].api_key; + + if (apiKey === "changeme") { + logger.warn( + "Default API Key of 'changeme' detected. Please change your API Key via the `/config/update` route!" + ); + } + + try { + DockStatAPI.listen( + process.env.DOCKSTATAPI_PORT || 3000, + ({ hostname, port }) => { + console.log("----- [ ############## ]"); + logger.info(`DockStatAPI is running at http://${hostname}:${port}`); + logger.info( + `Swagger API Documentation available at http://${hostname}:${port}/swagger` + ); + logger.info(`License: ${license}`); + logger.info(`Author: ${authorWebsite}`); + logger.info(`Contributors: ${contributors}`); + } + ); + } catch (error) { + logger.error("Failed to start server:", error); + process.exit(1); + } + } catch (error) { + logger.error("Error while starting server:", error); + process.exit(1); + } } await startServer(); From 6418d568a1de09904cc7b13cf1cc3174d1386ad6 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Wed, 30 Apr 2025 20:09:19 +0000 Subject: [PATCH 291/324] CQL: Apply lint fixes [skip ci] --- src/index.ts | 274 +++++++++++++++++++++++++-------------------------- 1 file changed, 137 insertions(+), 137 deletions(-) diff --git a/src/index.ts b/src/index.ts index 2780d10..57dc887 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,9 +9,9 @@ import { setSchedules } from "~/core/docker/scheduler"; import { loadPlugins } from "~/core/plugins/loader"; import { logger } from "~/core/utils/logger"; import { - authorWebsite, - contributors, - license, + authorWebsite, + contributors, + license, } from "~/core/utils/package-json"; import { swaggerReadme } from "~/core/utils/swagger-readme"; @@ -34,142 +34,142 @@ console.log(""); logger.info("Starting DockStatAPI"); export const DockStatAPI = new Elysia() - .use(staticPlugin()) - .use(serverTiming()) - .use( - swagger({ - documentation: { - info: { - title: "DockStatAPI", - version: "3.0.0", - description: swaggerReadme, - }, - components: { - securitySchemes: { - apiKeyAuth: { - type: "apiKey", - name: "x-api-key", - in: "header", - description: "API key for authentication", - }, - }, - }, - security: [ - { - apiKeyAuth: [], - }, - ], - tags: [ - { - name: "Statistics", - description: - "All endpoints for fetching statistics of hosts / containers", - }, - { - name: "Management", - description: "Various endpoints for managing DockStatAPI", - }, - { - name: "Stacks", - description: "DockStat's Stack functionality", - }, - { - name: "Utils", - description: "Various utilities which might be useful", - }, - ], - }, - }) - ) - .onBeforeHandle(async (context) => { - const { path, request, set } = context; - - if ( - path === "/health" || - path.startsWith("/swagger") || - path.startsWith("/trpc") - ) { - logger.info(`Requested unguarded route: ${path}`); - return; - } - - const validation = await validateApiKey(request, set); - - if (validation.error) { - set.status = 400; - set.headers["Content-Type"] = "application/json"; - return { error: validation.error }; - } - }) - .use(dockerRoutes) - .use(dockerStatsRoutes) - .use(backendLogs) - .use(dockerWebsocketRoutes) - .use(apiConfigRoutes) - .use(utilRoutes) - .use(stackRoutes) - .use(utilRoutes) - .use(liveLogs) - .use(liveStacks) - .get("/health", () => ({ status: "healthy" }), { tags: ["Utils"] }) - .onError(({ code, set, path }) => { - if (code === "NOT_FOUND") { - logger.warn(`Unknown route (${path}), showing error page!`); - set.status = 404; - set.headers["Content-Type"] = "text/html"; - return Bun.file("public/404.html"); - } - }); + .use(staticPlugin()) + .use(serverTiming()) + .use( + swagger({ + documentation: { + info: { + title: "DockStatAPI", + version: "3.0.0", + description: swaggerReadme, + }, + components: { + securitySchemes: { + apiKeyAuth: { + type: "apiKey", + name: "x-api-key", + in: "header", + description: "API key for authentication", + }, + }, + }, + security: [ + { + apiKeyAuth: [], + }, + ], + tags: [ + { + name: "Statistics", + description: + "All endpoints for fetching statistics of hosts / containers", + }, + { + name: "Management", + description: "Various endpoints for managing DockStatAPI", + }, + { + name: "Stacks", + description: "DockStat's Stack functionality", + }, + { + name: "Utils", + description: "Various utilities which might be useful", + }, + ], + }, + }), + ) + .onBeforeHandle(async (context) => { + const { path, request, set } = context; + + if ( + path === "/health" || + path.startsWith("/swagger") || + path.startsWith("/trpc") + ) { + logger.info(`Requested unguarded route: ${path}`); + return; + } + + const validation = await validateApiKey(request, set); + + if (validation.error) { + set.status = 400; + set.headers["Content-Type"] = "application/json"; + return { error: validation.error }; + } + }) + .use(dockerRoutes) + .use(dockerStatsRoutes) + .use(backendLogs) + .use(dockerWebsocketRoutes) + .use(apiConfigRoutes) + .use(utilRoutes) + .use(stackRoutes) + .use(utilRoutes) + .use(liveLogs) + .use(liveStacks) + .get("/health", () => ({ status: "healthy" }), { tags: ["Utils"] }) + .onError(({ code, set, path }) => { + if (code === "NOT_FOUND") { + logger.warn(`Unknown route (${path}), showing error page!`); + set.status = 404; + set.headers["Content-Type"] = "text/html"; + return Bun.file("public/404.html"); + } + }); async function startServer() { - try { - try { - await loadPlugins("./src/plugins"); - } catch (error) { - throw new Error(`Failed to load plugins: ${error}`); - } - - try { - await setSchedules(); - } catch (error) { - throw new Error(`Failed to set schedules: ${error}`); - } - - monitorDockerEvents().catch((error) => { - logger.error(`Monitoring Error: ${error}`); - }); - - const configData = dbFunctions.getConfig() as config[]; - const apiKey = configData[0].api_key; - - if (apiKey === "changeme") { - logger.warn( - "Default API Key of 'changeme' detected. Please change your API Key via the `/config/update` route!" - ); - } - - try { - DockStatAPI.listen( - process.env.DOCKSTATAPI_PORT || 3000, - ({ hostname, port }) => { - console.log("----- [ ############## ]"); - logger.info(`DockStatAPI is running at http://${hostname}:${port}`); - logger.info( - `Swagger API Documentation available at http://${hostname}:${port}/swagger` - ); - logger.info(`License: ${license}`); - logger.info(`Author: ${authorWebsite}`); - logger.info(`Contributors: ${contributors}`); - } - ); - } catch (error) { - logger.error("Failed to start server:", error); - process.exit(1); - } - } catch (error) { - logger.error("Error while starting server:", error); - process.exit(1); - } + try { + try { + await loadPlugins("./src/plugins"); + } catch (error) { + throw new Error(`Failed to load plugins: ${error}`); + } + + try { + await setSchedules(); + } catch (error) { + throw new Error(`Failed to set schedules: ${error}`); + } + + monitorDockerEvents().catch((error) => { + logger.error(`Monitoring Error: ${error}`); + }); + + const configData = dbFunctions.getConfig() as config[]; + const apiKey = configData[0].api_key; + + if (apiKey === "changeme") { + logger.warn( + "Default API Key of 'changeme' detected. Please change your API Key via the `/config/update` route!", + ); + } + + try { + DockStatAPI.listen( + process.env.DOCKSTATAPI_PORT || 3000, + ({ hostname, port }) => { + console.log("----- [ ############## ]"); + logger.info(`DockStatAPI is running at http://${hostname}:${port}`); + logger.info( + `Swagger API Documentation available at http://${hostname}:${port}/swagger`, + ); + logger.info(`License: ${license}`); + logger.info(`Author: ${authorWebsite}`); + logger.info(`Contributors: ${contributors}`); + }, + ); + } catch (error) { + logger.error("Failed to start server:", error); + process.exit(1); + } + } catch (error) { + logger.error("Error while starting server:", error); + process.exit(1); + } } await startServer(); From 10a1921c8d16aa5fe6d43803af6bb4ce674fbfe0 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sat, 3 May 2025 08:27:29 +0200 Subject: [PATCH 292/324] Fix: Formatting --- bun.lock | 420 ------------ package.json | 108 +-- src/core/utils/calculations.ts | 4 +- src/index.js | 1 + src/index.ts | 280 ++++---- src/routes/api-config.ts | 1145 ++++++++++++++++---------------- src/routes/docker-stats.ts | 772 ++++++++++++--------- tsconfig.json | 2 +- 8 files changed, 1236 insertions(+), 1496 deletions(-) delete mode 100644 bun.lock create mode 100644 src/index.js diff --git a/bun.lock b/bun.lock deleted file mode 100644 index ad696a2..0000000 --- a/bun.lock +++ /dev/null @@ -1,420 +0,0 @@ -{ - "lockfileVersion": 1, - "workspaces": { - "": { - "name": "dockstatapi", - "dependencies": { - "@elysiajs/server-timing": "^1.2.1", - "@elysiajs/static": "^1.2.0", - "@elysiajs/swagger": "^1.2.2", - "chalk": "^5.4.1", - "docker-compose": "^1.2.0", - "dockerode": "^4.0.5", - "elysia": "latest", - "knip": "latest", - "split2": "^4.2.0", - "winston": "^3.17.0", - "yaml": "^2.7.1", - }, - "devDependencies": { - "@biomejs/biome": "1.9.4", - "@types/dockerode": "^3.3.38", - "@types/node": "^22.14.1", - "@types/split2": "^4.2.3", - "bun-types": "latest", - "cross-env": "^7.0.3", - "logform": "^2.7.0", - "typescript": "^5.8.3", - "wrap-ansi": "^9.0.0", - }, - }, - }, - "trustedDependencies": [ - "protobufjs", - ], - "packages": { - "@balena/dockerignore": ["@balena/dockerignore@1.0.2", "", {}, "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q=="], - - "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="], - - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="], - - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="], - - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="], - - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="], - - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="], - - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="], - - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="], - - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], - - "@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="], - - "@dabh/diagnostics": ["@dabh/diagnostics@2.0.3", "", { "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA=="], - - "@elysiajs/server-timing": ["@elysiajs/server-timing@1.2.1", "", { "peerDependencies": { "elysia": ">= 1.2.0" } }, "sha512-7i4xOSYRdgljxKg8fyyBPVtnwsjhvJBnJn4qpTiNXt6ElrW1V2FeV2rdhyw2AQagUknnfpbUXMeDLalPaDeaLQ=="], - - "@elysiajs/static": ["@elysiajs/static@1.2.0", "", { "dependencies": { "node-cache": "^5.1.2" }, "peerDependencies": { "elysia": ">= 1.2.0" } }, "sha512-oLpAi8c+maPpA0XhhK3BELaIjIG+nXg/K9p8cFfW4q5ayRD59a3MOMOOGgpiXZkHJzLPWcouhhyyLAYtaANW4g=="], - - "@elysiajs/swagger": ["@elysiajs/swagger@1.2.2", "", { "dependencies": { "@scalar/themes": "^0.9.52", "@scalar/types": "^0.0.12", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.2.0" } }, "sha512-DG0PbX/wzQNQ6kIpFFPCvmkkWTIbNWDS7lVLv3Puy6ONklF14B4NnbDfpYjX1hdSYKeCqKBBOuenh6jKm8tbYA=="], - - "@grpc/grpc-js": ["@grpc/grpc-js@1.13.3", "", { "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-FTXHdOoPbZrBjlVLHuKbDZnsTxXv2BlHF57xw6LuThXacXvtkahEPED0CKMk6obZDf65Hv4k3z62eyPNpvinIg=="], - - "@grpc/proto-loader": ["@grpc/proto-loader@0.7.13", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.2.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw=="], - - "@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="], - - "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], - - "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], - - "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - - "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], - - "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], - - "@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="], - - "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="], - - "@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="], - - "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], - - "@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="], - - "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], - - "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], - - "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="], - - "@scalar/openapi-types": ["@scalar/openapi-types@0.1.1", "", {}, "sha512-NMy3QNk6ytcCoPUGJH0t4NNr36OWXgZhA3ormr3TvhX1NDgoF95wFyodGVH8xiHeUyn2/FxtETm8UBLbB5xEmg=="], - - "@scalar/themes": ["@scalar/themes@0.9.86", "", { "dependencies": { "@scalar/types": "0.1.7" } }, "sha512-QUHo9g5oSWi+0Lm1vJY9TaMZRau8LHg+vte7q5BVTBnu6NuQfigCaN+ouQ73FqIVd96TwMO6Db+dilK1B+9row=="], - - "@scalar/types": ["@scalar/types@0.0.12", "", { "dependencies": { "@scalar/openapi-types": "0.1.1", "@unhead/schema": "^1.9.5" } }, "sha512-XYZ36lSEx87i4gDqopQlGCOkdIITHHEvgkuJFrXFATQs9zHARop0PN0g4RZYWj+ZpCUclOcaOjbCt8JGe22mnQ=="], - - "@sinclair/typebox": ["@sinclair/typebox@0.34.33", "", {}, "sha512-5HAV9exOMcXRUxo+9iYB5n09XxzCXnfy4VTNW4xnDv+FgjzAGY989C28BIdljKqmF+ZltUwujE3aossvcVtq6g=="], - - "@types/docker-modem": ["@types/docker-modem@3.0.6", "", { "dependencies": { "@types/node": "*", "@types/ssh2": "*" } }, "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg=="], - - "@types/dockerode": ["@types/dockerode@3.3.38", "", { "dependencies": { "@types/docker-modem": "*", "@types/node": "*", "@types/ssh2": "*" } }, "sha512-nnrcfUe2iR+RyOuz0B4bZgQwD9djQa9ADEjp7OAgBs10pYT0KSCtplJjcmBDJz0qaReX5T7GbE5i4VplvzUHvA=="], - - "@types/node": ["@types/node@22.14.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw=="], - - "@types/split2": ["@types/split2@4.2.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-59OXIlfUsi2k++H6CHgUQKEb2HKRokUA39HY1i1dS8/AIcqVjtAAFdf8u+HxTWK/4FUHMJQlKSZ4I6irCBJ1Zw=="], - - "@types/ssh2": ["@types/ssh2@1.15.5", "", { "dependencies": { "@types/node": "^18.11.18" } }, "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ=="], - - "@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="], - - "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - - "@unhead/schema": ["@unhead/schema@1.11.20", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA=="], - - "ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], - - "ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], - - "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - - "asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="], - - "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], - - "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - - "bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="], - - "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], - - "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - - "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], - - "buildcheck": ["buildcheck@0.0.6", "", {}, "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A=="], - - "bun-types": ["bun-types@1.2.9", "", { "dependencies": { "@types/node": "*", "@types/ws": "*" } }, "sha512-dk/kOEfQbajENN/D6FyiSgOKEuUi9PWfqKQJEgwKrCMWbjS/S6tEXp178mWvWAcUSYm9ArDlWHZKO3T/4cLXiw=="], - - "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], - - "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], - - "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], - - "clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="], - - "color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="], - - "color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], - - "color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], - - "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], - - "colorspace": ["colorspace@1.1.4", "", { "dependencies": { "color": "^3.1.3", "text-hex": "1.0.x" } }, "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w=="], - - "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], - - "cpu-features": ["cpu-features@0.0.10", "", { "dependencies": { "buildcheck": "~0.0.6", "nan": "^2.19.0" } }, "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA=="], - - "cross-env": ["cross-env@7.0.3", "", { "dependencies": { "cross-spawn": "^7.0.1" }, "bin": { "cross-env": "src/bin/cross-env.js", "cross-env-shell": "src/bin/cross-env-shell.js" } }, "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw=="], - - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - - "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], - - "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], - - "docker-compose": ["docker-compose@1.2.0", "", { "dependencies": { "yaml": "^2.2.2" } }, "sha512-wIU1eHk3Op7dFgELRdmOYlPYS4gP8HhH1ZmZa13QZF59y0fblzFDFmKPhyc05phCy2hze9OEvNZAsoljrs+72w=="], - - "docker-modem": ["docker-modem@5.0.6", "", { "dependencies": { "debug": "^4.1.1", "readable-stream": "^3.5.0", "split-ca": "^1.0.1", "ssh2": "^1.15.0" } }, "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ=="], - - "dockerode": ["dockerode@4.0.5", "", { "dependencies": { "@balena/dockerignore": "^1.0.2", "@grpc/grpc-js": "^1.11.1", "@grpc/proto-loader": "^0.7.13", "docker-modem": "^5.0.6", "protobufjs": "^7.3.2", "tar-fs": "~2.1.2", "uuid": "^10.0.0" } }, "sha512-ZPmKSr1k1571Mrh7oIBS/j0AqAccoecY2yH420ni5j1KyNMgnoTh4Nu4FWunh0HZIJmRSmSysJjBIpa/zyWUEA=="], - - "easy-table": ["easy-table@1.2.0", "", { "dependencies": { "ansi-regex": "^5.0.1" }, "optionalDependencies": { "wcwidth": "^1.0.1" } }, "sha512-OFzVOv03YpvtcWGe5AayU5G2hgybsg3iqA6drU8UaoZyB9jLGMTrz9+asnLp/E+6qPh88yEI1gvyZFZ41dmgww=="], - - "elysia": ["elysia@1.2.25", "", { "dependencies": { "@sinclair/typebox": "^0.34.27", "cookie": "^1.0.2", "memoirist": "^0.3.0", "openapi-types": "^12.1.3" }, "peerDependencies": { "typescript": ">= 5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-WsdQpORJvb4uszzeqYT0lg97knw1iBW1NTzJ1Jm57tiHg+DfAotlWXYbjmvQ039ssV0fYELDHinLLoUazZkEHg=="], - - "emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], - - "enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="], - - "end-of-stream": ["end-of-stream@1.4.4", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q=="], - - "enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="], - - "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], - - "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], - - "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], - - "fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="], - - "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], - - "fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="], - - "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], - - "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], - - "get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="], - - "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - - "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], - - "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], - - "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - - "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], - - "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], - - "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - - "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], - - "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], - - "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], - - "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - - "jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="], - - "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], - - "knip": ["knip@5.50.4", "", { "dependencies": { "@nodelib/fs.walk": "^1.2.3", "easy-table": "1.2.0", "enhanced-resolve": "^5.18.1", "fast-glob": "^3.3.3", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "minimist": "^1.2.8", "picocolors": "^1.1.0", "picomatch": "^4.0.1", "pretty-ms": "^9.0.0", "smol-toml": "^1.3.1", "strip-json-comments": "5.0.1", "zod": "^3.22.4", "zod-validation-error": "^3.0.3" }, "peerDependencies": { "@types/node": ">=18", "typescript": ">=5.0.4" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-In+GjPpd2P3IDZnBBP4QF27vhQOhuBkICiuN9j+DMOf/m/qAFLGcbvuAGxco8IDvf26pvBnfeSmm1f6iNCkgOA=="], - - "kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="], - - "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], - - "logform": ["logform@2.7.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="], - - "long": ["long@5.3.1", "", {}, "sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng=="], - - "memoirist": ["memoirist@0.3.0", "", {}, "sha512-wR+4chMgVPq+T6OOsk40u9Wlpw1Pjx66NMNiYxCQQ4EUJ7jDs3D9kTCeKdBOkvAiqXlHLVJlvYL01PvIJ1MPNg=="], - - "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], - - "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], - - "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - - "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], - - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "nan": ["nan@2.22.2", "", {}, "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ=="], - - "nanoid": ["nanoid@5.1.5", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw=="], - - "node-cache": ["node-cache@5.1.2", "", { "dependencies": { "clone": "2.x" } }, "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg=="], - - "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - - "one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="], - - "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], - - "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], - - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - - "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], - - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - - "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], - - "pretty-ms": ["pretty-ms@9.2.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg=="], - - "protobufjs": ["protobufjs@7.5.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-Z2E/kOY1QjoMlCytmexzYfDm/w5fKAiRwpSzGtdnXW1zC88Z2yXazHHrOtwCzn+7wSxyE8PYM4rvVcMphF9sOA=="], - - "pump": ["pump@3.0.2", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw=="], - - "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], - - "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - - "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], - - "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], - - "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], - - "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - - "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], - - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], - - "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - - "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], - - "smol-toml": ["smol-toml@1.3.1", "", {}, "sha512-tEYNll18pPKHroYSmLLrksq233j021G0giwW7P3D24jC54pQ5W5BXMsQ/Mvw1OJCmEYDgY+lrzT+3nNUtoNfXQ=="], - - "split-ca": ["split-ca@1.0.1", "", {}, "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ=="], - - "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], - - "ssh2": ["ssh2@1.16.0", "", { "dependencies": { "asn1": "^0.2.6", "bcrypt-pbkdf": "^1.0.2" }, "optionalDependencies": { "cpu-features": "~0.0.10", "nan": "^2.20.0" } }, "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg=="], - - "stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="], - - "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], - - "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], - - "strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], - - "strip-json-comments": ["strip-json-comments@5.0.1", "", {}, "sha512-0fk9zBqO67Nq5M/m45qHCJxylV/DhBlIOVExqgOMiCCrzrhU6tCibRXNqE3jwJLftzE9SNuZtYbpzcO+i9FiKw=="], - - "tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="], - - "tar-fs": ["tar-fs@2.1.2", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA=="], - - "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], - - "text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="], - - "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], - - "triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="], - - "tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="], - - "type-fest": ["type-fest@4.40.0", "", {}, "sha512-ABHZ2/tS2JkvH1PEjxFDTUWC8dB5OsIGZP4IFLhR293GqT5Y5qB1WwL2kMPYhQW9DVgVD8Hd7I8gjwPIf5GFkw=="], - - "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], - - "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - - "uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], - - "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], - - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - - "winston": ["winston@3.17.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw=="], - - "winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="], - - "wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="], - - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - - "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], - - "yaml": ["yaml@2.7.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ=="], - - "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], - - "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], - - "zhead": ["zhead@2.2.4", "", {}, "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag=="], - - "zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="], - - "zod-validation-error": ["zod-validation-error@3.4.0", "", { "peerDependencies": { "zod": "^3.18.0" } }, "sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ=="], - - "@scalar/themes/@scalar/types": ["@scalar/types@0.1.7", "", { "dependencies": { "@scalar/openapi-types": "0.2.0", "@unhead/schema": "^1.11.11", "nanoid": "^5.1.5", "type-fest": "^4.20.0", "zod": "^3.23.8" } }, "sha512-irIDYzTQG2KLvFbuTI8k2Pz/R4JR+zUUSykVTbEMatkzMmVFnn1VzNSMlODbadycwZunbnL2tA27AXed9URVjw=="], - - "@types/ssh2/@types/node": ["@types/node@18.19.86", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-fifKayi175wLyKyc5qUfyENhQ1dCNI1UNjp653d8kuYcPQN5JhX3dGuP/XmvPTg/xRBn1VTLpbmi+H/Mr7tLfQ=="], - - "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - - "color-string/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - - "defaults/clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], - - "easy-table/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - - "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.2.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA=="], - - "@types/ssh2/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], - - "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "cliui/wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - - "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "cliui/wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - } -} diff --git a/package.json b/package.json index a3e18f4..a13dd82 100644 --- a/package.json +++ b/package.json @@ -1,52 +1,58 @@ { - "name": "dockstatapi", - "author": { - "email": "info@itsnik.de", - "name": "ItsNik", - "url": "https://github.com/Its4Nik" - }, - "license": "CC BY-NC 4.0", - "contributors": [], - "description": "DockStatAPI is an API backend featuring plugins and more for DockStat", - "version": "3.0.0", - "scripts": { - "start": "cross-env NODE_ENV=production LOG_LEVEL=info bun run src/index.ts", - "start:docker": "bun run build:docker && docker run -p 3000:3000 --rm -d --name dockstatapi -v 'plugins:/DockStatAPI/src/plugins' dockstatapi:local", - "dev": "docker compose -f docker/docker-compose.dev.yaml up -d && cross-env NODE_ENV=dev bun run --watch src/index.ts", - "dev:clean": "bun dev ; echo '\nExiting...' ; bun clean", - "build": "bun build --target bun src/index.ts --outdir ./dist", - "build:docker": "docker build -f docker/Dockerfile . -t 'dockstatapi:local'", - "clean": "bun run clean:win || bun run clean:lin", - "clean:win": "node -e \"process.exit(process.platform === 'win32' ? 0 : 1)\" && cmd /c del /Q data/dockstatapi*.db* && echo 'success'", - "clean:lin": "node -e \"process.exit(process.platform !== 'win32' ? 0 : 1)\" && rm -f data/dockstatapi*.db* && echo 'success'", - "knip": "knip", - "lint": "biome check --formatter-enabled=true --linter-enabled=true --organize-imports-enabled=true --fix src", - "test": "bun test src/tests/**/*.test.ts" - }, - "dependencies": { - "@elysiajs/server-timing": "^1.2.1", - "@elysiajs/static": "^1.2.0", - "@elysiajs/swagger": "^1.2.2", - "chalk": "^5.4.1", - "docker-compose": "^1.2.0", - "dockerode": "^4.0.5", - "elysia": "latest", - "knip": "latest", - "split2": "^4.2.0", - "winston": "^3.17.0", - "yaml": "^2.7.1" - }, - "devDependencies": { - "@biomejs/biome": "1.9.4", - "@types/dockerode": "^3.3.38", - "@types/node": "^22.14.1", - "@types/split2": "^4.2.3", - "bun-types": "latest", - "cross-env": "^7.0.3", - "logform": "^2.7.0", - "typescript": "^5.8.3", - "wrap-ansi": "^9.0.0" - }, - "module": "src/index.js", - "trustedDependencies": ["protobufjs"] -} + "name": "dockstatapi", + "author": { + "email": "info@itsnik.de", + "name": "ItsNik", + "url": "https://github.com/Its4Nik" + }, + "license": "CC BY-NC 4.0", + "contributors": [], + "description": "DockStatAPI is an API backend featuring plugins and more for DockStat", + "version": "3.0.0", + "scripts": { + "start": "cross-env NODE_ENV=production LOG_LEVEL=info bun run src/index.ts", + "start:docker": "bun run build:docker && docker run -p 3000:3000 --rm -d --name dockstatapi -v 'plugins:/DockStatAPI/src/plugins' dockstatapi:local", + "dev": "docker compose -f docker/docker-compose.dev.yaml up -d && cross-env NODE_ENV=dev bun run --watch src/index.ts", + "dev:clean": "bun dev ; echo '\nExiting...' ; bun clean", + "build": "bun build --target bun src/index.ts --outdir ./dist", + "build:prod": "NODE_ENV=production bun build --no-native --compile --minify-whitespace --minify-syntax --target bun --outfile server ./src/index.ts", + "build:docker": "docker build -f docker/Dockerfile . -t 'dockstatapi:local'", + "clean": "bun run clean:win || bun run clean:lin", + "clean:win": "node -e \"process.exit(process.platform === 'win32' ? 0 : 1)\" && cmd /c del /Q data/dockstatapi*.db* && echo 'success'", + "clean:lin": "node -e \"process.exit(process.platform !== 'win32' ? 0 : 1)\" && rm -f data/dockstatapi*.db* && echo 'success'", + "knip": "knip", + "lint": "biome check --formatter-enabled=true --linter-enabled=true --organize-imports-enabled=true --fix src", + "test": "bun test src/tests/**/*.test.ts" + }, + "dependencies": { + "@elysiajs/server-timing": "^1.2.1", + "@elysiajs/static": "^1.2.0", + "@elysiajs/swagger": "^1.2.2", + "chalk": "^5.4.1", + "docker-compose": "^1.2.0", + "dockerode": "^4.0.6", + "elysia": "latest", + "knip": "latest", + "split2": "^4.2.0", + "winston": "^3.17.0", + "yaml": "^2.7.1" + }, + "devDependencies": { + "@biomejs/biome": "1.9.4", + "@types/dockerode": "^3.3.38", + "@types/node": "^22.15.3", + "@types/split2": "^4.2.3", + "bun-types": "latest", + "cross-env": "^7.0.3", + "logform": "^2.7.0", + "typescript": "^5.8.3", + "wrap-ansi": "^9.0.0", + "@types/bun": "latest" + }, + "module": "src/index.js", + "trustedDependencies": [ + "protobufjs" + ], + "type": "module", + "private": true +} \ No newline at end of file diff --git a/src/core/utils/calculations.ts b/src/core/utils/calculations.ts index 1b5c893..b640c47 100644 --- a/src/core/utils/calculations.ts +++ b/src/core/utils/calculations.ts @@ -21,7 +21,7 @@ const calculateCpuPercent = (stats: Docker.ContainerStats): number => { return 0.0000001; } - return data; + return data * 10; }; const calculateMemoryUsage = (stats: Docker.ContainerStats): number => { @@ -31,7 +31,7 @@ const calculateMemoryUsage = (stats: Docker.ContainerStats): number => { const data = (stats.memory_stats.usage / stats.memory_stats.limit) * 100; - return data; + return data ; }; export { calculateCpuPercent, calculateMemoryUsage }; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..f67b2c6 --- /dev/null +++ b/src/index.js @@ -0,0 +1 @@ +console.log("Hello via Bun!"); \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 57dc887..a9d72d8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ import { serverTiming } from "@elysiajs/server-timing"; import staticPlugin from "@elysiajs/static"; import { swagger } from "@elysiajs/swagger"; -import { Elysia } from "elysia"; +import { Elysia, t } from "elysia"; import { dbFunctions } from "~/core/database"; import { monitorDockerEvents } from "~/core/docker/monitor"; @@ -9,9 +9,9 @@ import { setSchedules } from "~/core/docker/scheduler"; import { loadPlugins } from "~/core/plugins/loader"; import { logger } from "~/core/utils/logger"; import { - authorWebsite, - contributors, - license, + authorWebsite, + contributors, + license, } from "~/core/utils/package-json"; import { swaggerReadme } from "~/core/utils/swagger-readme"; @@ -33,148 +33,130 @@ console.log(""); logger.info("Starting DockStatAPI"); -export const DockStatAPI = new Elysia() - .use(staticPlugin()) - .use(serverTiming()) - .use( - swagger({ - documentation: { - info: { - title: "DockStatAPI", - version: "3.0.0", - description: swaggerReadme, - }, - components: { - securitySchemes: { - apiKeyAuth: { - type: "apiKey", - name: "x-api-key", - in: "header", - description: "API key for authentication", - }, - }, - }, - security: [ - { - apiKeyAuth: [], - }, - ], - tags: [ - { - name: "Statistics", - description: - "All endpoints for fetching statistics of hosts / containers", - }, - { - name: "Management", - description: "Various endpoints for managing DockStatAPI", - }, - { - name: "Stacks", - description: "DockStat's Stack functionality", - }, - { - name: "Utils", - description: "Various utilities which might be useful", - }, - ], - }, - }), - ) - .onBeforeHandle(async (context) => { - const { path, request, set } = context; - - if ( - path === "/health" || - path.startsWith("/swagger") || - path.startsWith("/trpc") - ) { - logger.info(`Requested unguarded route: ${path}`); - return; - } - - const validation = await validateApiKey(request, set); - - if (validation.error) { - set.status = 400; - set.headers["Content-Type"] = "application/json"; - return { error: validation.error }; - } - }) - .use(dockerRoutes) - .use(dockerStatsRoutes) - .use(backendLogs) - .use(dockerWebsocketRoutes) - .use(apiConfigRoutes) - .use(utilRoutes) - .use(stackRoutes) - .use(utilRoutes) - .use(liveLogs) - .use(liveStacks) - .get("/health", () => ({ status: "healthy" }), { tags: ["Utils"] }) - .onError(({ code, set, path }) => { - if (code === "NOT_FOUND") { - logger.warn(`Unknown route (${path}), showing error page!`); - set.status = 404; - set.headers["Content-Type"] = "text/html"; - return Bun.file("public/404.html"); - } - }); - -async function startServer() { - try { - try { - await loadPlugins("./src/plugins"); - } catch (error) { - throw new Error(`Failed to load plugins: ${error}`); - } - - try { - await setSchedules(); - } catch (error) { - throw new Error(`Failed to set schedules: ${error}`); - } - - monitorDockerEvents().catch((error) => { - logger.error(`Monitoring Error: ${error}`); - }); - - const configData = dbFunctions.getConfig() as config[]; - const apiKey = configData[0].api_key; - - if (apiKey === "changeme") { - logger.warn( - "Default API Key of 'changeme' detected. Please change your API Key via the `/config/update` route!", - ); - } - - try { - DockStatAPI.listen( - process.env.DOCKSTATAPI_PORT || 3000, - ({ hostname, port }) => { - console.log("----- [ ############## ]"); - logger.info(`DockStatAPI is running at http://${hostname}:${port}`); - logger.info( - `Swagger API Documentation available at http://${hostname}:${port}/swagger`, - ); - logger.info(`License: ${license}`); - logger.info(`Author: ${authorWebsite}`); - logger.info(`Contributors: ${contributors}`); - }, - ); - } catch (error) { - logger.error("Failed to start server:", error); - process.exit(1); - } - } catch (error) { - logger.error("Error while starting server:", error); - process.exit(1); - } -} - -await startServer(); - -logger.info("Started server"); -console.log("----- [ ############## ]"); - -export type DockStatAPI = typeof DockStatAPI; +const DockStatAPI = new Elysia() + .use(staticPlugin()) + .use(serverTiming()) + .use( + swagger({ + documentation: { + info: { + title: "DockStatAPI", + version: "3.0.0", + description: swaggerReadme, + }, + components: { + securitySchemes: { + apiKeyAuth: { + type: "apiKey" as const, + name: "x-api-key", + in: "header", + description: "API key for authentication", + }, + }, + }, + security: [ + { + apiKeyAuth: [], + }, + ], + tags: [ + { + name: "Statistics", + description: + "All endpoints for fetching statistics of hosts / containers", + }, + { + name: "Management", + description: "Various endpoints for managing DockStatAPI", + }, + { + name: "Stacks", + description: "DockStat's Stack functionality", + }, + { + name: "Utils", + description: "Various utilities which might be useful", + }, + ], + }, + }) + ) + .onBeforeHandle(async (context) => { + const { path, request, set } = context; + + if ( + path === "/health" || + path.startsWith("/swagger") || + path.startsWith("/trpc") + ) { + logger.info(`Requested unguarded route: ${path}`); + return; + } + + const validation = await validateApiKey(request, set); + + if (validation.error) { + set.status = 400; + set.headers["Content-Type"] = "application/json"; + return { error: validation.error }; + } + }) + .onError(({ code, set, path }) => { + if (code === "NOT_FOUND") { + logger.warn(`Unknown route (${path}), showing error page!`); + set.status = 404; + set.headers["Content-Type"] = "text/html"; + return Bun.file("public/404.html"); + } + }) + .use(dockerRoutes) + .use(dockerStatsRoutes) + .use(backendLogs) + .use(dockerWebsocketRoutes) + .use(apiConfigRoutes) + .use(utilRoutes) + .use(stackRoutes) + .use(liveLogs) + .use(liveStacks) + .get("/health", () => ({ status: "healthy" }), { tags: ["Utils"] }) + .listen(process.env.DOCKSTATAPI_PORT || 3000, ({ hostname, port }) => { + console.log("----- [ ############## ]"); + logger.info(`DockStatAPI is running at http://${hostname}:${port}`); + logger.info( + `Swagger API Documentation available at http://${hostname}:${port}/swagger` + ); + logger.info(`License: ${license}`); + logger.info(`Author: ${authorWebsite}`); + logger.info(`Contributors: ${contributors}`); + }); + +const initializeServer = async () => { + try { + await loadPlugins("./src/plugins"); + await setSchedules(); + + monitorDockerEvents().catch((error) => { + logger.error(`Monitoring Error: ${error}`); + }); + + const configData = dbFunctions.getConfig() as config[]; + const apiKey = configData[0].api_key; + + if (apiKey === "changeme") { + logger.warn( + "Default API Key of 'changeme' detected. Please change your API Key via the `/config/update` route!" + ); + } + + logger.info("Started server"); + console.log("----- [ ############## ]"); + } catch (error) { + logger.error("Error while starting server:", error); + process.exit(1); + } +}; + +await initializeServer(); + +export { DockStatAPI }; +export type App = typeof DockStatAPI; diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index 5ccca3e..79749e6 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -1,18 +1,18 @@ -import { existsSync, readdir, readdirSync, unlinkSync } from "node:fs"; +import { existsSync, readdirSync, unlinkSync } from "node:fs"; import { Elysia, t } from "elysia"; import { dbFunctions } from "~/core/database"; import { pluginManager } from "~/core/plugins/plugin-manager"; import { logger } from "~/core/utils/logger"; import { - authorEmail, - authorName, - authorWebsite, - contributors, - dependencies, - description, - devDependencies, - license, - version, + authorEmail, + authorName, + authorWebsite, + contributors, + dependencies, + description, + devDependencies, + license, + version, } from "~/core/utils/package-json"; import { responseHandler } from "~/core/utils/response-handler"; @@ -21,576 +21,577 @@ import { hashApiKey } from "~/middleware/auth"; import type { config } from "~/typings/database"; export const apiConfigRoutes = new Elysia({ prefix: "/config" }) - .get( - "/", - async ({ set }) => { - try { - const data = dbFunctions.getConfig() as config[]; - const distinct = data[0]; - set.status = 200; - set.headers["Content-Type"] = "application/json"; - logger.debug("Fetched backend config"); - return distinct; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Error getting the DockStatAPI config", - ); - } - }, - { - detail: { - tags: ["Management"], - description: - "Returns current API configuration including data retention policies and security settings", - responses: { - "200": { - description: "Successfully retrieved configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - fetching_interval: { - type: "number", - example: 5, - }, - keep_data_for: { - type: "number", - example: 7, - }, - api_key: { - type: "string", - example: "hashed_api_key", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error getting the DockStatAPI config", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - .get( - "/plugins", - ({ set }) => { - try { - return pluginManager.getLoadedPlugins(); - } catch (error) { - return responseHandler.error( - set, - error as string, - "Error getting all registered plugins", - ); - } - }, - { - detail: { - tags: ["Management"], - description: - "Lists all active plugins with their registration details and status", - responses: { - "200": { - description: "Successfully retrieved plugins", - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "object", - properties: { - name: { - type: "string", - example: "example-plugin", - }, - version: { - type: "string", - example: "1.0.0", - }, - status: { - type: "string", - example: "active", - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving plugins", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error getting all registered plugins", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - .post( - "/update", - async ({ set, body }) => { - try { - const { fetching_interval, keep_data_for, api_key } = body; - set.headers["Content-Type"] = "application/json"; - dbFunctions.updateConfig( - fetching_interval, - keep_data_for, - await hashApiKey(api_key), - ); - return responseHandler.ok(set, "Updated DockStatAPI config"); - } catch (error) { - return responseHandler.error( - set, - "Error updating the DockStatAPI config", - error as string, - ); - } - }, - { - detail: { - tags: ["Management"], - description: - "Modifies core API settings including data collection intervals, retention periods, and security credentials", - responses: { - "200": { - description: "Successfully updated configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Updated DockStatAPI config", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error updating configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error updating the DockStatAPI config", - }, - }, - }, - }, - }, - }, - }, - }, - body: t.Object({ - fetching_interval: t.Number(), - keep_data_for: t.Number(), - api_key: t.String(), - }), - }, - ) - .get( - "/package", - async ({ set }) => { - try { - logger.debug("Fetching package.json"); - return { - version: version, - description: description, - license: license, - authorName: authorName, - authorEmail: authorEmail, - authorWebsite: authorWebsite, - contributors: contributors, - dependencies: dependencies, - devDependencies: devDependencies, - }; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Error while reading package.json", - ); - } - }, - { - detail: { - tags: ["Management"], - description: - "Displays package metadata including dependencies, contributors, and licensing information", - responses: { - "200": { - description: "Successfully retrieved package information", - content: { - "application/json": { - schema: { - type: "object", - properties: { - version: { - type: "string", - example: "3.0.0", - }, - description: { - type: "string", - example: - "DockStatAPI is an API backend featuring plugins and more for DockStat", - }, - license: { - type: "string", - example: "CC BY-NC 4.0", - }, - authorName: { - type: "string", - example: "ItsNik", - }, - authorEmail: { - type: "string", - example: "info@itsnik.de", - }, - authorWebsite: { - type: "string", - example: "https://github.com/Its4Nik", - }, - contributors: { - type: "array", - items: { - type: "string", - }, - example: [], - }, - dependencies: { - type: "object", - example: { - "@elysiajs/server-timing": "^1.2.1", - "@elysiajs/static": "^1.2.0", - }, - }, - devDependencies: { - type: "object", - example: { - "@biomejs/biome": "1.9.4", - "@types/dockerode": "^3.3.38", - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving package information", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error while reading package.json", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - .post( - "/backup", - async ({ set }) => { - try { - const backupFilename = await dbFunctions.backupDatabase(); - return responseHandler.ok(set, backupFilename); - } catch (error) { - return responseHandler.error(set, error as string, "Error backing up"); - } - }, - { - detail: { - tags: ["Management"], - description: "Backs up the internal database", - responses: { - "200": { - description: "Successfully created backup", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "backup_2024-03-20_12-00-00.db.bak", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error creating backup", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error backing up", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - .get( - "/backup", - async ({ set }) => { - try { - const backupFiles = readdirSync(backupDir); + .get( + "/", + async ({ set }) => { + try { + const data = dbFunctions.getConfig() as config[]; + const distinct = data[0]; + set.status = 200; + set.headers["Content-Type"] = "application/json"; + logger.debug("Fetched backend config"); + return distinct; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Error getting the DockStatAPI config" + ); + } + }, + { + detail: { + tags: ["Management"], + description: + "Returns current API configuration including data retention policies and security settings", + responses: { + "200": { + description: "Successfully retrieved configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + fetching_interval: { + type: "number", + example: 5, + }, + keep_data_for: { + type: "number", + example: 7, + }, + api_key: { + type: "string", + example: "hashed_api_key", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error getting the DockStatAPI config", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) + .get( + "/plugins", + ({ set }) => { + try { + return pluginManager.getLoadedPlugins(); + } catch (error) { + return responseHandler.error( + set, + error as string, + "Error getting all registered plugins" + ); + } + }, + { + detail: { + tags: ["Management"], + description: + "Lists all active plugins with their registration details and status", + responses: { + "200": { + description: "Successfully retrieved plugins", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "object", + properties: { + name: { + type: "string", + example: "example-plugin", + }, + version: { + type: "string", + example: "1.0.0", + }, + status: { + type: "string", + example: "active", + }, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving plugins", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error getting all registered plugins", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) + .post( + "/update", + async ({ set, body }) => { + try { + const { fetching_interval, keep_data_for, api_key } = body; + set.headers["Content-Type"] = "application/json"; + dbFunctions.updateConfig( + fetching_interval, + keep_data_for, + await hashApiKey(api_key) + ); + return responseHandler.ok(set, "Updated DockStatAPI config"); + } catch (error) { + return responseHandler.error( + set, + "Error updating the DockStatAPI config", + error as string + ); + } + }, + { + detail: { + tags: ["Management"], + description: + "Modifies core API settings including data collection intervals, retention periods, and security credentials", + responses: { + "200": { + description: "Successfully updated configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Updated DockStatAPI config", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error updating configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error updating the DockStatAPI config", + }, + }, + }, + }, + }, + }, + }, + }, + body: t.Object({ + fetching_interval: t.Number(), + keep_data_for: t.Number(), + api_key: t.String(), + }), + } + ) + .get( + "/package", + async ({ set }) => { + try { + logger.debug("Fetching package.json"); + return { + version: version, + description: description, + license: license, + authorName: authorName, + authorEmail: authorEmail, + authorWebsite: authorWebsite, + contributors: contributors, + dependencies: dependencies, + devDependencies: devDependencies, + }; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Error while reading package.json" + ); + } + }, + { + detail: { + tags: ["Management"], + description: + "Displays package metadata including dependencies, contributors, and licensing information", + responses: { + "200": { + description: "Successfully retrieved package information", + content: { + "application/json": { + schema: { + type: "object", + properties: { + version: { + type: "string", + example: "3.0.0", + }, + description: { + type: "string", + example: + "DockStatAPI is an API backend featuring plugins and more for DockStat", + }, + license: { + type: "string", + example: "CC BY-NC 4.0", + }, + authorName: { + type: "string", + example: "ItsNik", + }, + authorEmail: { + type: "string", + example: "info@itsnik.de", + }, + authorWebsite: { + type: "string", + example: "https://github.com/Its4Nik", + }, + contributors: { + type: "array", + items: { + type: "string", + }, + example: [], + }, + dependencies: { + type: "object", + example: { + "@elysiajs/server-timing": "^1.2.1", + "@elysiajs/static": "^1.2.0", + }, + }, + devDependencies: { + type: "object", + example: { + "@biomejs/biome": "1.9.4", + "@types/dockerode": "^3.3.38", + }, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving package information", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error while reading package.json", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) + .post( + "/backup", + async ({ set }) => { + try { + const backupFilename = await dbFunctions.backupDatabase(); + return responseHandler.ok(set, backupFilename); + } catch (error) { + return responseHandler.error(set, error as string, "Error backing up"); + } + }, + { + detail: { + tags: ["Management"], + description: "Backs up the internal database", + responses: { + "200": { + description: "Successfully created backup", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "backup_2024-03-20_12-00-00.db.bak", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error creating backup", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error backing up", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) + .get( + "/backup", + async ({ set }) => { + try { + const backupFiles = readdirSync(backupDir); - const filteredFiles = backupFiles.filter((file: string) => { - return !( - file.endsWith(".db") || - file.endsWith(".db-shm") || - file.endsWith(".db-wal") - ); - }); + const filteredFiles = backupFiles.filter((file: string) => { + return !( + file.endsWith(".db") || + file.endsWith(".db-shm") || + file.endsWith(".db-wal") + ); + }); - return filteredFiles; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Reading Backup directory", - ); - } - }, - { - detail: { - tags: ["Management"], - description: "Lists all available backups", - responses: { - "200": { - description: "Successfully retrieved backup list", - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "string", - }, - example: [ - "backup_2024-03-20_12-00-00.db.bak", - "backup_2024-03-19_12-00-00.db.bak", - ], - }, - }, - }, - }, - "400": { - description: "Error retrieving backup list", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Reading Backup directory", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) + return filteredFiles; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Reading Backup directory" + ); + } + }, + { + detail: { + tags: ["Management"], + description: "Lists all available backups", + responses: { + "200": { + description: "Successfully retrieved backup list", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "string", + }, + example: [ + "backup_2024-03-20_12-00-00.db.bak", + "backup_2024-03-19_12-00-00.db.bak", + ], + }, + }, + }, + }, + "400": { + description: "Error retrieving backup list", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Reading Backup directory", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) - .get( - "/backup/download", - async ({ query, set }) => { - try { - const filename = query.filename || dbFunctions.findLatestBackup(); - const filePath = `${backupDir}/${filename}`; + .get( + "/backup/download", + async ({ query, set }) => { + try { + const filename = query.filename || dbFunctions.findLatestBackup(); + const filePath = `${backupDir}/${filename}`; - if (!existsSync(filePath)) { - throw new Error("Backup file not found"); - } + if (!existsSync(filePath)) { + throw new Error("Backup file not found"); + } - set.headers["Content-Type"] = "application/octet-stream"; - set.headers["Content-Disposition"] = - `attachment; filename="${filename}"`; - return Bun.file(filePath); - } catch (error) { - return responseHandler.error( - set, - error as string, - "Backup download failed", - ); - } - }, - { - detail: { - tags: ["Management"], - description: - "Download a specific backup or the latest if no filename is provided", - responses: { - "200": { - description: "Successfully downloaded backup file", - content: { - "application/octet-stream": { - schema: { - type: "string", - format: "binary", - example: "Binary backup file content", - }, - }, - }, - headers: { - "Content-Disposition": { - schema: { - type: "string", - example: - 'attachment; filename="backup_2024-03-20_12-00-00.db.bak"', - }, - }, - }, - }, - "400": { - description: "Error downloading backup", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Backup download failed", - }, - }, - }, - }, - }, - }, - }, - }, - query: t.Object({ - filename: t.Optional(t.String()), - }), - }, - ) - .post( - "/restore", - async ({ body, set }) => { - try { - const { file } = body; + set.headers["Content-Type"] = "application/octet-stream"; + set.headers[ + "Content-Disposition" + ] = `attachment; filename="${filename}"`; + return Bun.file(filePath); + } catch (error) { + return responseHandler.error( + set, + error as string, + "Backup download failed" + ); + } + }, + { + detail: { + tags: ["Management"], + description: + "Download a specific backup or the latest if no filename is provided", + responses: { + "200": { + description: "Successfully downloaded backup file", + content: { + "application/octet-stream": { + schema: { + type: "string", + format: "binary", + example: "Binary backup file content", + }, + }, + }, + headers: { + "Content-Disposition": { + schema: { + type: "string", + example: + 'attachment; filename="backup_2024-03-20_12-00-00.db.bak"', + }, + }, + }, + }, + "400": { + description: "Error downloading backup", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Backup download failed", + }, + }, + }, + }, + }, + }, + }, + }, + query: t.Object({ + filename: t.Optional(t.String()), + }), + } + ) + .post( + "/restore", + async ({ body, set }) => { + try { + const { file } = body; - set.headers["Content-Type"] = "text/html"; + set.headers["Content-Type"] = "text/html"; - if (!file) { - throw new Error("No file uploaded"); - } + if (!file) { + throw new Error("No file uploaded"); + } - if (!file.name.endsWith(".db.bak")) { - throw new Error("Invalid file type. Expected .db.bak"); - } + if (!file.name.endsWith(".db.bak")) { + throw new Error("Invalid file type. Expected .db.bak"); + } - const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; - const fileBuffer = await file.arrayBuffer(); + const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; + const fileBuffer = await file.arrayBuffer(); - await Bun.write(tempPath, fileBuffer); - dbFunctions.restoreDatabase(tempPath); - unlinkSync(tempPath); + await Bun.write(tempPath, fileBuffer); + dbFunctions.restoreDatabase(tempPath); + unlinkSync(tempPath); - return responseHandler.ok(set, "Database restored successfully"); - } catch (error) { - return responseHandler.error( - set, - error instanceof Error ? error.message : "Restoration failed", - "Database restoration error", - ); - } - }, - { - body: t.Object({ file: t.File() }), - detail: { - tags: ["Management"], - description: "Restore database from uploaded backup file", - responses: { - "200": { - description: "Successfully restored database", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Database restored successfully", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error restoring database", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Database restoration error", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ); + return responseHandler.ok(set, "Database restored successfully"); + } catch (error) { + return responseHandler.error( + set, + error instanceof Error ? error.message : "Restoration failed", + "Database restoration error" + ); + } + }, + { + body: t.Object({ file: t.File() }), + detail: { + tags: ["Management"], + description: "Restore database from uploaded backup file", + responses: { + "200": { + description: "Successfully restored database", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Database restored successfully", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error restoring database", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Database restoration error", + }, + }, + }, + }, + }, + }, + }, + }, + } + ); diff --git a/src/routes/docker-stats.ts b/src/routes/docker-stats.ts index 3781f26..42b887d 100644 --- a/src/routes/docker-stats.ts +++ b/src/routes/docker-stats.ts @@ -4,8 +4,8 @@ import { Elysia } from "elysia"; import { dbFunctions } from "~/core/database"; import { getDockerClient } from "~/core/docker/client"; import { - calculateCpuPercent, - calculateMemoryUsage, + calculateCpuPercent, + calculateMemoryUsage, } from "~/core/utils/calculations"; import { findObjectByKey } from "~/core/utils/helpers"; import { logger } from "~/core/utils/logger"; @@ -15,311 +15,481 @@ import type { ContainerInfo, DockerHost, HostStats } from "~/typings/docker"; import type { DockerInfo } from "~/typings/dockerode"; export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) - .get( - "/containers", - async ({ set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const containers: ContainerInfo[] = []; + .get( + "/containers", + async ({ set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + const containers: ContainerInfo[] = []; - await Promise.all( - hosts.map(async (host) => { - try { - const docker = getDockerClient(host); - try { - await docker.ping(); - } catch (pingError) { - return responseHandler.error( - set, - pingError as string, - "Docker host connection failed", - ); - } + await Promise.all( + hosts.map(async (host) => { + try { + const docker = getDockerClient(host); + try { + await docker.ping(); + } catch (pingError) { + return responseHandler.error( + set, + pingError as string, + "Docker host connection failed" + ); + } - const hostContainers = await docker.listContainers({ all: true }); + const hostContainers = await docker.listContainers({ all: true }); - await Promise.all( - hostContainers.map(async (containerInfo) => { - try { - const container = docker.getContainer(containerInfo.Id); - const stats = await new Promise( - (resolve, reject) => { - container.stats({ stream: false }, (error, stats) => { - if (error) { - return responseHandler.reject( - set, - reject, - "An error occurred", - error, - ); - } - if (!stats) { - return responseHandler.reject( - set, - reject, - "No stats available", - ); - } - resolve(stats); - }); - }, - ); + await Promise.all( + hostContainers.map(async (containerInfo) => { + try { + const container = docker.getContainer(containerInfo.Id); + const stats = await new Promise( + (resolve, reject) => { + container.stats({ stream: false }, (error, stats) => { + if (error) { + return responseHandler.reject( + set, + reject, + "An error occurred", + error + ); + } + if (!stats) { + return responseHandler.reject( + set, + reject, + "No stats available" + ); + } + resolve(stats); + }); + } + ); - containers.push({ - id: containerInfo.Id, - hostId: `${host.id}`, - name: containerInfo.Names[0].replace(/^\//, ""), - image: containerInfo.Image, - status: containerInfo.Status, - state: containerInfo.State, - cpuUsage: calculateCpuPercent(stats), - memoryUsage: calculateMemoryUsage(stats), - stats: stats, - info: containerInfo, - }); - } catch (containerError) { - logger.error( - "Error fetching container stats,", - containerError, - ); - } - }), - ); - logger.debug(`Fetched stats for ${host.name}`); - } catch (hostError) { - logger.error("Error fetching containers for host,", hostError); - } - }), - ); + containers.push({ + id: containerInfo.Id, + hostId: `${host.id}`, + name: containerInfo.Names[0].replace(/^\//, ""), + image: containerInfo.Image, + status: containerInfo.Status, + state: containerInfo.State, + cpuUsage: calculateCpuPercent(stats), + memoryUsage: calculateMemoryUsage(stats), + stats: stats, + info: containerInfo, + }); + } catch (containerError) { + logger.error( + "Error fetching container stats,", + containerError + ); + } + }) + ); + logger.debug(`Fetched stats for ${host.name}`); + } catch (hostError) { + logger.error("Error fetching containers for host,", hostError); + } + }) + ); - set.headers["Content-Type"] = "application/json"; - logger.debug("Fetched all containers across all hosts"); - return { containers }; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to retrieve containers", - ); - } - }, - { - detail: { - tags: ["Statistics"], - description: - "Collects real-time statistics for all Docker containers across monitored hosts, including CPU and memory utilization", - responses: { - "200": { - description: "Successfully retrieved container statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - containers: { - type: "array", - items: { - type: "object", - properties: { - id: { - type: "string", - example: "abc123def456", - }, - hostId: { - type: "string", - example: "1", - }, - name: { - type: "string", - example: "example-container", - }, - image: { - type: "string", - example: "nginx:latest", - }, - status: { - type: "string", - example: "running", - }, - state: { - type: "string", - example: "running", - }, - cpuUsage: { - type: "number", - example: 0.5, - }, - memoryUsage: { - type: "number", - example: 1024, - }, - }, - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving container statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve containers", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) + set.headers["Content-Type"] = "application/json"; + logger.debug("Fetched all containers across all hosts"); + return { containers }; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to retrieve containers" + ); + } + }, + { + detail: { + tags: ["Statistics"], + description: + "Collects real-time statistics for all Docker containers across monitored hosts, including CPU and memory utilization", + responses: { + "200": { + description: "Successfully retrieved container statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + containers: { + type: "array", + items: { + type: "object", + properties: { + id: { + type: "string", + example: "abc123def456", + }, + hostId: { + type: "string", + example: "1", + }, + name: { + type: "string", + example: "example-container", + }, + image: { + type: "string", + example: "nginx:latest", + }, + status: { + type: "string", + example: "running", + }, + state: { + type: "string", + example: "running", + }, + cpuUsage: { + type: "number", + example: 0.5, + }, + memoryUsage: { + type: "number", + example: 1024, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving container statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve containers", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) + .get( + "/hosts", + async ({ set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - .get( - "/hosts/:id", - async ({ params, set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const host = findObjectByKey(hosts, "name", params.id); - if (!host) { - return responseHandler.simple_error( - set, - `Host (${params.id}) not found`, - ); - } + const stats: HostStats[] = []; - const docker = getDockerClient(host); - const info: DockerInfo = await docker.info(); + for (const host of hosts) { + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); - const config: HostStats = { - hostId: host.id as number, - hostName: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - labels: info.Labels, - images: info.Images, - containers: info.Containers, - containersPaused: info.ContainersPaused, - containersRunning: info.ContainersRunning, - containersStopped: info.ContainersStopped, - }; + const config: HostStats = { + hostId: host.id as number, + hostName: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; - set.headers["Content-Type"] = "application/json"; - logger.debug(`Fetched config for ${host.name}`); - return config; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to retrieve host config", - ); - } - }, - { - detail: { - tags: ["Statistics"], - description: - "Provides detailed system metrics and Docker runtime information for specified host", - responses: { - "200": { - description: "Successfully retrieved host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - hostId: { - type: "number", - example: 1, - }, - hostName: { - type: "string", - example: "Localhost", - }, - dockerVersion: { - type: "string", - example: "24.0.5", - }, - apiVersion: { - type: "string", - example: "1.41", - }, - os: { - type: "string", - example: "Linux", - }, - architecture: { - type: "string", - example: "x86_64", - }, - totalMemory: { - type: "number", - example: 16777216, - }, - totalCPU: { - type: "number", - example: 4, - }, - labels: { - type: "array", - items: { - type: "string", - }, - example: ["environment=production"], - }, - images: { - type: "number", - example: 10, - }, - containers: { - type: "number", - example: 5, - }, - containersPaused: { - type: "number", - example: 0, - }, - containersRunning: { - type: "number", - example: 4, - }, - containersStopped: { - type: "number", - example: 1, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve host config", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ); + stats.push(config); + } + + set.headers["Content-Type"] = "application/json"; + logger.debug("Fetched all hosts"); + return stats; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to retrieve host config" + ); + } + }, + { + detail: { + tags: ["Statistics"], + description: + "Provides detailed system metrics and Docker runtime information for specified host", + responses: { + "200": { + description: "Successfully retrieved host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + hostId: { + type: "number", + example: 1, + }, + hostName: { + type: "string", + example: "Localhost", + }, + dockerVersion: { + type: "string", + example: "24.0.5", + }, + apiVersion: { + type: "string", + example: "1.41", + }, + os: { + type: "string", + example: "Linux", + }, + architecture: { + type: "string", + example: "x86_64", + }, + totalMemory: { + type: "number", + example: 16777216, + }, + totalCPU: { + type: "number", + example: 4, + }, + labels: { + type: "array", + items: { + type: "string", + }, + example: ["environment=production"], + }, + images: { + type: "number", + example: 10, + }, + containers: { + type: "number", + example: 5, + }, + containersPaused: { + type: "number", + example: 0, + }, + containersRunning: { + type: "number", + example: 4, + }, + containersStopped: { + type: "number", + example: 1, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve host config", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) + .get( + "/hosts/:id", + async ({ params, set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + + if (!params.id) { + const stats: HostStats[] = []; + + for (const host of hosts) { + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); + + const config: HostStats = { + hostId: host.id as number, + hostName: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; + + stats.push(config); + } + + return stats; + } + + const host = findObjectByKey(hosts, "id", Number(params.id)); + if (!host) { + return responseHandler.simple_error( + set, + `Host (${params.id}) not found` + ); + } + + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); + + const config: HostStats = { + hostId: host.id as number, + hostName: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; + + set.headers["Content-Type"] = "application/json"; + logger.debug(`Fetched config for ${host.name}`); + return config; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to retrieve host config" + ); + } + }, + { + detail: { + tags: ["Statistics"], + description: + "Provides detailed system metrics and Docker runtime information for specified host", + responses: { + "200": { + description: "Successfully retrieved host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + hostId: { + type: "number", + example: 1, + }, + hostName: { + type: "string", + example: "Localhost", + }, + dockerVersion: { + type: "string", + example: "24.0.5", + }, + apiVersion: { + type: "string", + example: "1.41", + }, + os: { + type: "string", + example: "Linux", + }, + architecture: { + type: "string", + example: "x86_64", + }, + totalMemory: { + type: "number", + example: 16777216, + }, + totalCPU: { + type: "number", + example: 4, + }, + labels: { + type: "array", + items: { + type: "string", + }, + example: ["environment=production"], + }, + images: { + type: "number", + example: 10, + }, + containers: { + type: "number", + example: 5, + }, + containersPaused: { + type: "number", + example: 0, + }, + containersRunning: { + type: "number", + example: 4, + }, + containersStopped: { + type: "number", + example: 1, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve host config", + }, + }, + }, + }, + }, + }, + }, + }, + } + ); diff --git a/tsconfig.json b/tsconfig.json index 3a44e36..fc07b9b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ /* Projects */ // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ - // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ From 595002baedb90f2a26ab68c39ddfb428c3710c7c Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sat, 3 May 2025 08:27:59 +0200 Subject: [PATCH 293/324] Fix: Remove index.js --- src/index.js | 1 - 1 file changed, 1 deletion(-) delete mode 100644 src/index.js diff --git a/src/index.js b/src/index.js deleted file mode 100644 index f67b2c6..0000000 --- a/src/index.js +++ /dev/null @@ -1 +0,0 @@ -console.log("Hello via Bun!"); \ No newline at end of file From b561168aa69a3d40e7598822acd1b1548a617ac9 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Sat, 3 May 2025 06:28:02 +0000 Subject: [PATCH 294/324] Update dependency graphs --- dependency-graph.mmd | 208 ++++--- dependency-graph.svg | 1339 +++++++++++++++++++++--------------------- 2 files changed, 765 insertions(+), 782 deletions(-) diff --git a/dependency-graph.mmd b/dependency-graph.mmd index dcab557..1920bf8 100644 --- a/dependency-graph.mmd +++ b/dependency-graph.mmd @@ -11,23 +11,23 @@ subgraph 0["src"] subgraph 5["routes"] 6["live-stacks.ts"] U["live-logs.ts"] -1H["api-config.ts"] -1J["docker-manager.ts"] -1K["docker-stats.ts"] -1L["docker-websocket.ts"] -1N["logs.ts"] -1O["stacks.ts"] -1R["utils.ts"] +1G["api-config.ts"] +1I["docker-manager.ts"] +1J["docker-stats.ts"] +1K["docker-websocket.ts"] +1M["logs.ts"] +1N["stacks.ts"] +1Q["utils.ts"] end subgraph 8["core"] subgraph 9["utils"] A["logger.ts"] T["helpers.ts"] -15["calculations.ts"] -19["change-me-checker.ts"] -1B["package-json.ts"] -1D["swagger-readme.ts"] -1I["response-handler.ts"] +14["calculations.ts"] +18["change-me-checker.ts"] +1A["package-json.ts"] +1C["swagger-readme.ts"] +1H["response-handler.ts"] end subgraph C["database"] D["_dbState.ts"] @@ -44,21 +44,21 @@ R["stacks.ts"] end subgraph V["docker"] W["monitor.ts"] -12["client.ts"] -13["scheduler.ts"] -14["store-container-stats.ts"] -16["store-host-stats.ts"] +11["client.ts"] +12["scheduler.ts"] +13["store-container-stats.ts"] +15["store-host-stats.ts"] end -subgraph Y["plugins"] -Z["plugin-manager.ts"] -18["loader.ts"] +subgraph X["plugins"] +Y["plugin-manager.ts"] +17["loader.ts"] end -subgraph 1P["stacks"] -1Q["controller.ts"] +subgraph 1O["stacks"] +1P["controller.ts"] end end -subgraph 1E["middleware"] -1F["auth.ts"] +subgraph 1D["middleware"] +1E["auth.ts"] end end subgraph 2["~"] @@ -68,37 +68,36 @@ subgraph 3["typings"] G["misc"] O["docker"] S["docker-compose"] -10["plugin"] -17["dockerode"] -1G["elysiajs"] +Z["plugin"] +16["dockerode"] +1F["elysiajs"] end end B["path"] subgraph H["fs"] -1A["promises"] +19["promises"] end J["bun:sqlite"] -X["bun"] -11["events"] -1C["package.json"] -1M["stream"] +10["events"] +1B["package.json"] +1L["stream"] 1-->6 1-->E 1-->W -1-->13 -1-->18 +1-->12 +1-->17 1-->A -1-->1B -1-->1D -1-->1F -1-->1H +1-->1A +1-->1C +1-->1E +1-->1G +1-->1I 1-->1J 1-->1K -1-->1L 1-->U +1-->1M 1-->1N -1-->1O -1-->1R +1-->1Q 1-->4 6-->A 6-->7 @@ -146,87 +145,86 @@ R-->S T-->A U-->A U-->4 -W-->Z +W-->Y W-->E -W-->12 +W-->11 W-->A W-->O -W-->X -Z-->A -Z-->O -Z-->10 -Z-->11 +Y-->A +Y-->O +Y-->Z +Y-->10 +11-->A +11-->O +12-->E +12-->13 +12-->15 12-->A -12-->O +12-->4 +13-->A 13-->E +13-->11 13-->14 -13-->16 -13-->A -13-->4 -14-->A -14-->E -14-->12 -14-->15 -16-->E -16-->12 -16-->T -16-->A -16-->O -16-->17 -18-->19 +15-->E +15-->11 +15-->T +15-->A +15-->O +15-->16 +17-->18 +17-->A +17-->Y +17-->H +17-->B 18-->A -18-->Z -18-->H -18-->B -19-->A -19-->1A -1B-->1C -1F-->E -1F-->A -1F-->4 -1F-->1G -1H-->E -1H-->F -1H-->Z +18-->19 +1A-->1B +1E-->E +1E-->A +1E-->4 +1E-->1F +1G-->E +1G-->F +1G-->Y +1G-->A +1G-->1A +1G-->1H +1G-->1E +1G-->4 +1G-->H 1H-->A -1H-->1B -1H-->1I 1H-->1F -1H-->4 -1H-->H +1I-->E 1I-->A -1I-->1G +1I-->1H +1I-->O 1J-->E +1J-->11 +1J-->14 +1J-->T 1J-->A -1J-->1I +1J-->1H 1J-->O +1J-->16 1K-->E -1K-->12 -1K-->15 -1K-->T +1K-->11 +1K-->14 1K-->A -1K-->1I -1K-->O -1K-->17 -1L-->E -1L-->12 -1L-->15 -1L-->A -1L-->1I -1L-->1M +1K-->1H +1K-->1L +1M-->E +1M-->A 1N-->E +1N-->1P 1N-->A -1O-->E -1O-->1Q -1O-->A -1O-->1I -1Q-->T -1Q-->E -1Q-->A -1Q-->6 -1Q-->4 -1Q-->S +1N-->1H +1P-->T +1P-->E +1P-->A +1P-->6 +1P-->4 +1P-->S +1P-->19 1Q-->1A -1R-->1B -1R-->1I +1Q-->1H diff --git a/dependency-graph.svg b/dependency-graph.svg index 7a12bb4..092821e 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -4,60 +4,60 @@ - - + + dependency-cruiser output - + cluster_fs - -fs + +fs cluster_src - -src + +src cluster_src/core - -core + +core cluster_src/core/database - -database + +database cluster_src/core/docker - -docker + +docker cluster_src/core/plugins - -plugins + +plugins cluster_src/core/stacks - -stacks + +stacks cluster_src/core/utils - -utils + +utils cluster_src/middleware - -middleware + +middleware cluster_src/routes - -routes + +routes cluster_~ @@ -69,1406 +69,1391 @@ typings - - -bun - - -bun - - - - + bun:sqlite - - -bun:sqlite + + +bun:sqlite - + events - - -events + + +events - + fs - - -fs + + +fs - + fs/promises - - -promises + + +promises - + package.json - - -package.json + + +package.json - + path - - -path + + +path - + src/core/database/_dbState.ts - - -_dbState.ts + + +_dbState.ts - + src/core/database/backup.ts - - -backup.ts + + +backup.ts src/core/database/backup.ts->fs - - + + src/core/database/backup.ts->src/core/database/_dbState.ts - - + + - + src/core/database/database.ts - - -database.ts + + +database.ts src/core/database/backup.ts->src/core/database/database.ts - - + + - + src/core/database/helper.ts - - -helper.ts + + +helper.ts src/core/database/backup.ts->src/core/database/helper.ts - - - - + + + + - + src/core/utils/logger.ts - - -logger.ts + + +logger.ts src/core/database/backup.ts->src/core/utils/logger.ts - - - - + + + + - + ~/typings/misc - - -misc + + +misc src/core/database/backup.ts->~/typings/misc - - + + src/core/database/database.ts->bun:sqlite - - + + src/core/database/database.ts->fs - - + + src/core/database/helper.ts->src/core/database/_dbState.ts - - + + src/core/database/helper.ts->src/core/utils/logger.ts - - - - + + + + - + src/core/utils/logger.ts->path - - + + - + src/core/utils/logger.ts->src/core/database/_dbState.ts - - + + - + src/core/database/index.ts - - -index.ts + + +index.ts - + src/core/utils/logger.ts->src/core/database/index.ts - - - - + + + + - + ~/typings/database - + database - + src/core/utils/logger.ts->~/typings/database - - + + - + src/routes/live-logs.ts - - -live-logs.ts + + +live-logs.ts - + src/core/utils/logger.ts->src/routes/live-logs.ts - - - - + + + + - + src/core/database/config.ts - - -config.ts + + +config.ts src/core/database/config.ts->src/core/database/database.ts - - + + src/core/database/config.ts->src/core/database/helper.ts - - - - + + + + - + src/core/database/containerStats.ts - - -containerStats.ts + + +containerStats.ts src/core/database/containerStats.ts->src/core/database/database.ts - - + + src/core/database/containerStats.ts->src/core/database/helper.ts - - - - + + + + - + src/core/database/dockerHosts.ts - - -dockerHosts.ts + + +dockerHosts.ts src/core/database/dockerHosts.ts->src/core/database/database.ts - - + + src/core/database/dockerHosts.ts->src/core/database/helper.ts - - - - + + + + - + ~/typings/docker - - -docker + + +docker src/core/database/dockerHosts.ts->~/typings/docker - - + + - + src/core/database/hostStats.ts - - -hostStats.ts + + +hostStats.ts src/core/database/hostStats.ts->src/core/database/database.ts - - + + src/core/database/hostStats.ts->src/core/database/helper.ts - - - - + + + + src/core/database/hostStats.ts->~/typings/docker - - + + src/core/database/index.ts->src/core/database/backup.ts - - - - + + + + src/core/database/index.ts->src/core/database/database.ts - - + + src/core/database/index.ts->src/core/database/config.ts - - - - + + + + src/core/database/index.ts->src/core/database/containerStats.ts - - - - + + + + src/core/database/index.ts->src/core/database/dockerHosts.ts - - - - + + + + src/core/database/index.ts->src/core/database/hostStats.ts - - - - + + + + - + src/core/database/logs.ts - - -logs.ts + + +logs.ts src/core/database/index.ts->src/core/database/logs.ts - - - - + + + + - + src/core/database/stacks.ts - - -stacks.ts + + +stacks.ts src/core/database/index.ts->src/core/database/stacks.ts - - - - + + + + src/core/database/logs.ts->src/core/database/database.ts - - + + src/core/database/logs.ts->src/core/database/helper.ts - - - - + + + + src/core/database/logs.ts->~/typings/database - - + + src/core/database/stacks.ts->src/core/database/database.ts - - + + src/core/database/stacks.ts->src/core/database/helper.ts - - - - + + + + src/core/database/stacks.ts->~/typings/database - - + + - + src/core/utils/helpers.ts - - -helpers.ts + + +helpers.ts src/core/database/stacks.ts->src/core/utils/helpers.ts - - - - + + + + - + ~/typings/docker-compose - - -docker-compose + + +docker-compose src/core/database/stacks.ts->~/typings/docker-compose - - + + - + src/core/utils/helpers.ts->src/core/utils/logger.ts - - - - + + + + - + src/core/docker/client.ts - - -client.ts + + +client.ts src/core/docker/client.ts->src/core/utils/logger.ts - - + + src/core/docker/client.ts->~/typings/docker - - + + - + src/core/docker/monitor.ts - - -monitor.ts + + +monitor.ts - - -src/core/docker/monitor.ts->bun - - - src/core/docker/monitor.ts->src/core/utils/logger.ts - - + + src/core/docker/monitor.ts->~/typings/docker - - + + src/core/docker/monitor.ts->src/core/database/index.ts - - + + src/core/docker/monitor.ts->src/core/docker/client.ts - - + + - + src/core/plugins/plugin-manager.ts - - -plugin-manager.ts + + +plugin-manager.ts src/core/docker/monitor.ts->src/core/plugins/plugin-manager.ts - - + + - + src/core/plugins/plugin-manager.ts->events - - + + - + src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts - - + + - + src/core/plugins/plugin-manager.ts->~/typings/docker - - + + - + ~/typings/plugin - - -plugin + + +plugin - + src/core/plugins/plugin-manager.ts->~/typings/plugin - - + + - + src/core/docker/scheduler.ts - - -scheduler.ts + + +scheduler.ts - + src/core/docker/scheduler.ts->src/core/utils/logger.ts - - + + - + src/core/docker/scheduler.ts->src/core/database/index.ts - - + + - + src/core/docker/scheduler.ts->~/typings/database - - + + - + src/core/docker/store-container-stats.ts - - -store-container-stats.ts + + +store-container-stats.ts - + src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts - - + + - + src/core/docker/store-host-stats.ts - - -store-host-stats.ts + + +store-host-stats.ts - + src/core/docker/scheduler.ts->src/core/docker/store-host-stats.ts - - + + - + src/core/docker/store-container-stats.ts->src/core/utils/logger.ts - - + + - + src/core/docker/store-container-stats.ts->src/core/database/index.ts - - + + - + src/core/docker/store-container-stats.ts->src/core/docker/client.ts - - + + - + src/core/utils/calculations.ts - - -calculations.ts + + +calculations.ts - + src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/utils/logger.ts - - + + - + src/core/docker/store-host-stats.ts->~/typings/docker - - + + - + src/core/docker/store-host-stats.ts->src/core/database/index.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/utils/helpers.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/docker/client.ts - - + + - + ~/typings/dockerode - + dockerode - + src/core/docker/store-host-stats.ts->~/typings/dockerode - - + + - + src/core/plugins/loader.ts - - -loader.ts + + +loader.ts - + src/core/plugins/loader.ts->fs - - + + - + src/core/plugins/loader.ts->path - - + + - + src/core/plugins/loader.ts->src/core/utils/logger.ts - - + + - + src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts - - + + - + src/core/utils/change-me-checker.ts - - -change-me-checker.ts + + +change-me-checker.ts - + src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts - - + + - + src/core/utils/change-me-checker.ts->fs/promises - - + + - + src/core/utils/change-me-checker.ts->src/core/utils/logger.ts - - + + - + src/core/stacks/controller.ts - - -controller.ts + + +controller.ts - + src/core/stacks/controller.ts->fs/promises - - + + - + src/core/stacks/controller.ts->src/core/utils/logger.ts - - + + - + src/core/stacks/controller.ts->src/core/database/index.ts - - + + - + src/core/stacks/controller.ts->~/typings/database - - + + - + src/core/stacks/controller.ts->src/core/utils/helpers.ts - - + + - + src/core/stacks/controller.ts->~/typings/docker-compose - - + + - + src/routes/live-stacks.ts - - -live-stacks.ts + + +live-stacks.ts - + src/core/stacks/controller.ts->src/routes/live-stacks.ts - - + + - + src/routes/live-stacks.ts->src/core/utils/logger.ts - - + + - + ~/typings/websocket - + websocket - + src/routes/live-stacks.ts->~/typings/websocket - + - + src/routes/live-logs.ts->src/core/utils/logger.ts - - - - + + + + - + src/routes/live-logs.ts->~/typings/database - - + + - + src/core/utils/package-json.ts - - -package-json.ts + + +package-json.ts - + src/core/utils/package-json.ts->package.json - - + + - + src/core/utils/response-handler.ts - - -response-handler.ts + + +response-handler.ts - + src/core/utils/response-handler.ts->src/core/utils/logger.ts - - + + - + ~/typings/elysiajs - - -elysiajs + + +elysiajs - + src/core/utils/response-handler.ts->~/typings/elysiajs - - + + - + src/core/utils/swagger-readme.ts - - -swagger-readme.ts + + +swagger-readme.ts - + src/index.ts - - -index.ts + + +index.ts - + src/index.ts->src/core/utils/logger.ts - - + + - + src/index.ts->src/core/database/index.ts - - + + - + src/index.ts->~/typings/database - - + + - + src/index.ts->src/core/docker/monitor.ts - - + + - + src/index.ts->src/core/docker/scheduler.ts - - + + - + src/index.ts->src/core/plugins/loader.ts - - + + - + src/index.ts->src/routes/live-stacks.ts - - + + - + src/index.ts->src/routes/live-logs.ts - - + + - + src/index.ts->src/core/utils/package-json.ts - - + + - + src/index.ts->src/core/utils/swagger-readme.ts - - + + - + src/middleware/auth.ts - - -auth.ts + + +auth.ts - + src/index.ts->src/middleware/auth.ts - - + + - + src/routes/api-config.ts - - -api-config.ts + + +api-config.ts - + src/index.ts->src/routes/api-config.ts - - + + - + src/routes/docker-manager.ts - - -docker-manager.ts + + +docker-manager.ts - + src/index.ts->src/routes/docker-manager.ts - - + + - + src/routes/docker-stats.ts - - -docker-stats.ts + + +docker-stats.ts - + src/index.ts->src/routes/docker-stats.ts - - + + - + src/routes/docker-websocket.ts - - -docker-websocket.ts + + +docker-websocket.ts - + src/index.ts->src/routes/docker-websocket.ts - - + + - + src/routes/logs.ts - - -logs.ts + + +logs.ts - + src/index.ts->src/routes/logs.ts - - + + - + src/routes/stacks.ts - - -stacks.ts + + +stacks.ts - + src/index.ts->src/routes/stacks.ts - - + + - + src/routes/utils.ts - - -utils.ts + + +utils.ts - + src/index.ts->src/routes/utils.ts - - + + - + src/middleware/auth.ts->src/core/utils/logger.ts - - + + - + src/middleware/auth.ts->src/core/database/index.ts - - + + - + src/middleware/auth.ts->~/typings/database - - + + - + src/middleware/auth.ts->~/typings/elysiajs - - + + - + src/routes/api-config.ts->fs - - + + - + src/routes/api-config.ts->src/core/database/backup.ts - - + + - + src/routes/api-config.ts->src/core/utils/logger.ts - - + + - + src/routes/api-config.ts->src/core/database/index.ts - - + + - + src/routes/api-config.ts->~/typings/database - - + + - + src/routes/api-config.ts->src/core/plugins/plugin-manager.ts - - + + - + src/routes/api-config.ts->src/core/utils/package-json.ts - - + + - + src/routes/api-config.ts->src/core/utils/response-handler.ts - - + + - + src/routes/api-config.ts->src/middleware/auth.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-manager.ts->~/typings/docker - - + + - + src/routes/docker-manager.ts->src/core/database/index.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/response-handler.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-stats.ts->~/typings/docker - - + + - + src/routes/docker-stats.ts->src/core/database/index.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/helpers.ts - - + + - + src/routes/docker-stats.ts->src/core/docker/client.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-stats.ts->~/typings/dockerode - - + + - + src/routes/docker-stats.ts->src/core/utils/response-handler.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-websocket.ts->src/core/database/index.ts - - + + - + src/routes/docker-websocket.ts->src/core/docker/client.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/response-handler.ts - - + + - + stream - - -stream + + +stream - + src/routes/docker-websocket.ts->stream - - + + - + src/routes/logs.ts->src/core/utils/logger.ts - - + + - + src/routes/logs.ts->src/core/database/index.ts - - + + - + src/routes/stacks.ts->src/core/utils/logger.ts - - + + - + src/routes/stacks.ts->src/core/database/index.ts - - + + - + src/routes/stacks.ts->src/core/stacks/controller.ts - - + + - + src/routes/stacks.ts->src/core/utils/response-handler.ts - - + + - + src/routes/utils.ts->src/core/utils/package-json.ts - - + + - + src/routes/utils.ts->src/core/utils/response-handler.ts - - + + From 2bfdcce62d679b4e93194d1c2af381af3ff45c97 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 6 May 2025 20:13:41 +0200 Subject: [PATCH 295/324] Feat: Elysia server.d.ts route --- package.json | 1 + src/core/utils/swagger-readme.ts | 2 ++ src/index.ts | 15 ++++++++++----- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index a13dd82..3217c54 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "docker-compose": "^1.2.0", "dockerode": "^4.0.6", "elysia": "latest", + "elysia-remote-dts": "^1.0.2", "knip": "latest", "split2": "^4.2.0", "winston": "^3.17.0", diff --git a/src/core/utils/swagger-readme.ts b/src/core/utils/swagger-readme.ts index ff30a8b..c1457c6 100644 --- a/src/core/utils/swagger-readme.ts +++ b/src/core/utils/swagger-readme.ts @@ -1,4 +1,6 @@ export const swaggerReadme: string = ` +[Download API type sheet](/server.d.ts) + ![Docker](https://img.shields.io/badge/Docker-2CA5E0?style=flat&logo=docker&logoColor=white) ![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=flat&logo=typescript&logoColor=white) diff --git a/src/index.ts b/src/index.ts index a9d72d8..e2d478e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ import { serverTiming } from "@elysiajs/server-timing"; import staticPlugin from "@elysiajs/static"; import { swagger } from "@elysiajs/swagger"; import { Elysia, t } from "elysia"; - +import { dts } from "elysia-remote-dts"; import { dbFunctions } from "~/core/database"; import { monitorDockerEvents } from "~/core/docker/monitor"; import { setSchedules } from "~/core/docker/scheduler"; @@ -14,9 +14,7 @@ import { license, } from "~/core/utils/package-json"; import { swaggerReadme } from "~/core/utils/swagger-readme"; - import { validateApiKey } from "~/middleware/auth"; - import { apiConfigRoutes } from "~/routes/api-config"; import { dockerRoutes } from "~/routes/docker-manager"; import { dockerStatsRoutes } from "~/routes/docker-stats"; @@ -25,9 +23,8 @@ import { liveLogs } from "~/routes/live-logs"; import { backendLogs } from "~/routes/logs"; import { stackRoutes } from "~/routes/stacks"; import { utilRoutes } from "~/routes/utils"; -import { liveStacks } from "./routes/live-stacks"; - import type { config } from "~/typings/database"; +import { liveStacks } from "./routes/live-stacks"; console.log(""); @@ -36,6 +33,14 @@ logger.info("Starting DockStatAPI"); const DockStatAPI = new Elysia() .use(staticPlugin()) .use(serverTiming()) + .use( + dts("./src/index.ts", { + tsconfig: "./tsconfig.json", + compilerOptions: { + strict: true, + }, + }) + ) .use( swagger({ documentation: { From 675c3a104ec3746256840580844eeb9787a55537 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Tue, 6 May 2025 18:15:07 +0000 Subject: [PATCH 296/324] Update dependency graphs --- dependency-graph.mmd | 390 ++++++++++++++++++++-------------------- dependency-graph.svg | 411 ++++++++++++++++++++++--------------------- 2 files changed, 409 insertions(+), 392 deletions(-) diff --git a/dependency-graph.mmd b/dependency-graph.mmd index 1920bf8..e54cc0b 100644 --- a/dependency-graph.mmd +++ b/dependency-graph.mmd @@ -8,223 +8,225 @@ flowchart LR subgraph 0["src"] 1["index.ts"] -subgraph 5["routes"] -6["live-stacks.ts"] -U["live-logs.ts"] -1G["api-config.ts"] -1I["docker-manager.ts"] -1J["docker-stats.ts"] -1K["docker-websocket.ts"] -1M["logs.ts"] -1N["stacks.ts"] -1Q["utils.ts"] +subgraph 6["routes"] +7["live-stacks.ts"] +V["live-logs.ts"] +1H["api-config.ts"] +1J["docker-manager.ts"] +1K["docker-stats.ts"] +1L["docker-websocket.ts"] +1N["logs.ts"] +1O["stacks.ts"] +1R["utils.ts"] end -subgraph 8["core"] -subgraph 9["utils"] -A["logger.ts"] -T["helpers.ts"] -14["calculations.ts"] -18["change-me-checker.ts"] -1A["package-json.ts"] -1C["swagger-readme.ts"] -1H["response-handler.ts"] +subgraph 9["core"] +subgraph A["utils"] +B["logger.ts"] +U["helpers.ts"] +15["calculations.ts"] +19["change-me-checker.ts"] +1B["package-json.ts"] +1D["swagger-readme.ts"] +1I["response-handler.ts"] end -subgraph C["database"] -D["_dbState.ts"] -E["index.ts"] -F["backup.ts"] -I["database.ts"] -K["helper.ts"] -L["config.ts"] -M["containerStats.ts"] -N["dockerHosts.ts"] -P["hostStats.ts"] -Q["logs.ts"] -R["stacks.ts"] +subgraph D["database"] +E["_dbState.ts"] +F["index.ts"] +G["backup.ts"] +J["database.ts"] +L["helper.ts"] +M["config.ts"] +N["containerStats.ts"] +O["dockerHosts.ts"] +Q["hostStats.ts"] +R["logs.ts"] +S["stacks.ts"] end -subgraph V["docker"] -W["monitor.ts"] -11["client.ts"] -12["scheduler.ts"] -13["store-container-stats.ts"] -15["store-host-stats.ts"] +subgraph W["docker"] +X["monitor.ts"] +12["client.ts"] +13["scheduler.ts"] +14["store-container-stats.ts"] +16["store-host-stats.ts"] end -subgraph X["plugins"] -Y["plugin-manager.ts"] -17["loader.ts"] +subgraph Y["plugins"] +Z["plugin-manager.ts"] +18["loader.ts"] end -subgraph 1O["stacks"] -1P["controller.ts"] +subgraph 1P["stacks"] +1Q["controller.ts"] end end -subgraph 1D["middleware"] -1E["auth.ts"] +subgraph 1E["middleware"] +1F["auth.ts"] end end subgraph 2["~"] subgraph 3["typings"] 4["database"] -7["websocket"] -G["misc"] -O["docker"] -S["docker-compose"] -Z["plugin"] -16["dockerode"] -1F["elysiajs"] +8["websocket"] +H["misc"] +P["docker"] +T["docker-compose"] +10["plugin"] +17["dockerode"] +1G["elysiajs"] end end -B["path"] -subgraph H["fs"] -19["promises"] +5["elysia-remote-dts"] +C["path"] +subgraph I["fs"] +1A["promises"] end -J["bun:sqlite"] -10["events"] -1B["package.json"] -1L["stream"] -1-->6 -1-->E -1-->W -1-->12 -1-->17 -1-->A -1-->1A -1-->1C -1-->1E -1-->1G -1-->1I +K["bun:sqlite"] +11["events"] +1C["package.json"] +1M["stream"] +1-->7 +1-->F +1-->X +1-->13 +1-->18 +1-->B +1-->1B +1-->1D +1-->1F +1-->1H 1-->1J 1-->1K -1-->U -1-->1M +1-->1L +1-->V 1-->1N -1-->1Q +1-->1O +1-->1R 1-->4 -6-->A -6-->7 -A-->D -A-->E -A-->U -A-->4 -A-->B -E-->F -E-->L -E-->M -E-->I -E-->N -E-->P -E-->Q -E-->R -F-->D -F-->I -F-->K -F-->A +1-->5 +7-->B +7-->8 +B-->E +B-->F +B-->V +B-->4 +B-->C F-->G -F-->H -I-->J -I-->H -K-->D -K-->A -L-->I -L-->K -M-->I -M-->K -N-->I -N-->K -N-->O -P-->I -P-->K -P-->O -Q-->I -Q-->K -Q-->4 -R-->T -R-->I -R-->K +F-->M +F-->N +F-->J +F-->O +F-->Q +F-->R +F-->S +G-->E +G-->J +G-->L +G-->B +G-->H +G-->I +J-->K +J-->I +L-->E +L-->B +M-->J +M-->L +N-->J +N-->L +O-->J +O-->L +O-->P +Q-->J +Q-->L +Q-->P +R-->J +R-->L R-->4 -R-->S -T-->A -U-->A -U-->4 -W-->Y -W-->E -W-->11 -W-->A -W-->O -Y-->A -Y-->O -Y-->Z -Y-->10 -11-->A -11-->O -12-->E -12-->13 -12-->15 -12-->A -12-->4 -13-->A -13-->E -13-->11 +S-->U +S-->J +S-->L +S-->4 +S-->T +U-->B +V-->B +V-->4 +X-->Z +X-->F +X-->12 +X-->B +X-->P +Z-->B +Z-->P +Z-->10 +Z-->11 +12-->B +12-->P +13-->F 13-->14 -15-->E -15-->11 -15-->T -15-->A -15-->O -15-->16 -17-->18 -17-->A -17-->Y -17-->H -17-->B -18-->A +13-->16 +13-->B +13-->4 +14-->B +14-->F +14-->12 +14-->15 +16-->F +16-->12 +16-->U +16-->B +16-->P +16-->17 18-->19 -1A-->1B -1E-->E -1E-->A -1E-->4 -1E-->1F -1G-->E -1G-->F -1G-->Y -1G-->A -1G-->1A -1G-->1H -1G-->1E -1G-->4 -1G-->H -1H-->A +18-->B +18-->Z +18-->I +18-->C +19-->B +19-->1A +1B-->1C +1F-->F +1F-->B +1F-->4 +1F-->1G +1H-->F +1H-->G +1H-->Z +1H-->B +1H-->1B +1H-->1I 1H-->1F -1I-->E -1I-->A -1I-->1H -1I-->O -1J-->E -1J-->11 -1J-->14 -1J-->T -1J-->A -1J-->1H -1J-->O -1J-->16 -1K-->E -1K-->11 -1K-->14 -1K-->A -1K-->1H -1K-->1L -1M-->E -1M-->A -1N-->E -1N-->1P -1N-->A -1N-->1H -1P-->T -1P-->E -1P-->A -1P-->6 -1P-->4 -1P-->S -1P-->19 +1H-->4 +1H-->I +1I-->B +1I-->1G +1J-->F +1J-->B +1J-->1I +1J-->P +1K-->F +1K-->12 +1K-->15 +1K-->U +1K-->B +1K-->1I +1K-->P +1K-->17 +1L-->F +1L-->12 +1L-->15 +1L-->B +1L-->1I +1L-->1M +1N-->F +1N-->B +1O-->F +1O-->1Q +1O-->B +1O-->1I +1Q-->U +1Q-->F +1Q-->B +1Q-->7 +1Q-->4 +1Q-->T 1Q-->1A -1Q-->1H +1R-->1B +1R-->1I diff --git a/dependency-graph.svg b/dependency-graph.svg index 092821e..6e3dd9e 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -78,64 +78,73 @@ - + +elysia-remote-dts + + +elysia-remote-dts + + + + + events - + events - + fs - + fs - + fs/promises - + promises - + package.json - + package.json - + path - + path - + src/core/database/_dbState.ts - + _dbState.ts - + src/core/database/backup.ts - + backup.ts @@ -154,9 +163,9 @@ - + src/core/database/database.ts - + database.ts @@ -169,9 +178,9 @@ - + src/core/database/helper.ts - + helper.ts @@ -186,9 +195,9 @@ - + src/core/utils/logger.ts - + logger.ts @@ -203,9 +212,9 @@ - + ~/typings/misc - + misc @@ -256,9 +265,9 @@ - + src/core/database/index.ts - + index.ts @@ -273,9 +282,9 @@ - + ~/typings/database - + database @@ -288,9 +297,9 @@ - + src/routes/live-logs.ts - + live-logs.ts @@ -305,9 +314,9 @@ - + src/core/database/config.ts - + config.ts @@ -328,9 +337,9 @@ - + src/core/database/containerStats.ts - + containerStats.ts @@ -351,9 +360,9 @@ - + src/core/database/dockerHosts.ts - + dockerHosts.ts @@ -374,9 +383,9 @@ - + ~/typings/docker - + docker @@ -389,9 +398,9 @@ - + src/core/database/hostStats.ts - + hostStats.ts @@ -464,9 +473,9 @@ - + src/core/database/logs.ts - + logs.ts @@ -481,9 +490,9 @@ - + src/core/database/stacks.ts - + stacks.ts @@ -538,9 +547,9 @@ - + src/core/utils/helpers.ts - + helpers.ts @@ -555,9 +564,9 @@ - + ~/typings/docker-compose - + docker-compose @@ -578,9 +587,9 @@ - + src/core/docker/client.ts - + client.ts @@ -599,9 +608,9 @@ - + src/core/docker/monitor.ts - + monitor.ts @@ -628,13 +637,13 @@ src/core/docker/monitor.ts->src/core/docker/client.ts - - + + - + src/core/plugins/plugin-manager.ts - + plugin-manager.ts @@ -665,9 +674,9 @@ - + ~/typings/plugin - + plugin @@ -680,9 +689,9 @@ - + src/core/docker/scheduler.ts - + scheduler.ts @@ -707,9 +716,9 @@ - + src/core/docker/store-container-stats.ts - + store-container-stats.ts @@ -722,9 +731,9 @@ - + src/core/docker/store-host-stats.ts - + store-host-stats.ts @@ -751,13 +760,13 @@ src/core/docker/store-container-stats.ts->src/core/docker/client.ts - - + + - + src/core/utils/calculations.ts - + calculations.ts @@ -800,9 +809,9 @@ - + ~/typings/dockerode - + dockerode @@ -815,9 +824,9 @@ - + src/core/plugins/loader.ts - + loader.ts @@ -826,19 +835,19 @@ src/core/plugins/loader.ts->fs - + src/core/plugins/loader.ts->path - + src/core/plugins/loader.ts->src/core/utils/logger.ts - + @@ -848,9 +857,9 @@ - + src/core/utils/change-me-checker.ts - + change-me-checker.ts @@ -875,9 +884,9 @@ - + src/core/stacks/controller.ts - + controller.ts @@ -920,9 +929,9 @@ - + src/routes/live-stacks.ts - + live-stacks.ts @@ -931,32 +940,32 @@ src/core/stacks/controller.ts->src/routes/live-stacks.ts - - + + - + src/routes/live-stacks.ts->src/core/utils/logger.ts - + ~/typings/websocket - + websocket - + src/routes/live-stacks.ts->~/typings/websocket - + src/routes/live-logs.ts->src/core/utils/logger.ts @@ -964,15 +973,15 @@ - + src/routes/live-logs.ts->~/typings/database - - + + - + src/core/utils/package-json.ts - + package-json.ts @@ -985,9 +994,9 @@ - + src/core/utils/response-handler.ts - + response-handler.ts @@ -1000,9 +1009,9 @@ - + ~/typings/elysiajs - + elysiajs @@ -1015,87 +1024,93 @@ - + src/core/utils/swagger-readme.ts - + swagger-readme.ts - + src/index.ts - - -index.ts + + +index.ts + + +src/index.ts->elysia-remote-dts + + + src/index.ts->src/core/utils/logger.ts - + src/index.ts->src/core/database/index.ts - + src/index.ts->~/typings/database - - + + src/index.ts->src/core/docker/monitor.ts - - + + src/index.ts->src/core/docker/scheduler.ts - - + + src/index.ts->src/core/plugins/loader.ts - - + + src/index.ts->src/routes/live-stacks.ts - - + + src/index.ts->src/routes/live-logs.ts - - + + src/index.ts->src/core/utils/package-json.ts - - + + src/index.ts->src/core/utils/swagger-readme.ts - + - + src/middleware/auth.ts - + auth.ts @@ -1104,13 +1119,13 @@ src/index.ts->src/middleware/auth.ts - - + + - + src/routes/api-config.ts - + api-config.ts @@ -1119,13 +1134,13 @@ src/index.ts->src/routes/api-config.ts - - + + - + src/routes/docker-manager.ts - + docker-manager.ts @@ -1134,13 +1149,13 @@ src/index.ts->src/routes/docker-manager.ts - - + + - + src/routes/docker-stats.ts - + docker-stats.ts @@ -1149,13 +1164,13 @@ src/index.ts->src/routes/docker-stats.ts - - + + - + src/routes/docker-websocket.ts - + docker-websocket.ts @@ -1164,13 +1179,13 @@ src/index.ts->src/routes/docker-websocket.ts - + - + src/routes/logs.ts - + logs.ts @@ -1179,13 +1194,13 @@ src/index.ts->src/routes/logs.ts - - + + - + src/routes/stacks.ts - + stacks.ts @@ -1194,13 +1209,13 @@ src/index.ts->src/routes/stacks.ts - - + + - + src/routes/utils.ts - + utils.ts @@ -1209,248 +1224,248 @@ src/index.ts->src/routes/utils.ts - - + + - + src/middleware/auth.ts->src/core/utils/logger.ts - + src/middleware/auth.ts->src/core/database/index.ts - + src/middleware/auth.ts->~/typings/database - + src/middleware/auth.ts->~/typings/elysiajs - + src/routes/api-config.ts->fs - + src/routes/api-config.ts->src/core/database/backup.ts - + src/routes/api-config.ts->src/core/utils/logger.ts - + src/routes/api-config.ts->src/core/database/index.ts - + src/routes/api-config.ts->~/typings/database - + src/routes/api-config.ts->src/core/plugins/plugin-manager.ts - + src/routes/api-config.ts->src/core/utils/package-json.ts - + src/routes/api-config.ts->src/core/utils/response-handler.ts - + src/routes/api-config.ts->src/middleware/auth.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/logger.ts - + src/routes/docker-manager.ts->~/typings/docker - + src/routes/docker-manager.ts->src/core/database/index.ts - + src/routes/docker-manager.ts->src/core/utils/response-handler.ts - + src/routes/docker-stats.ts->src/core/utils/logger.ts - + src/routes/docker-stats.ts->~/typings/docker - + src/routes/docker-stats.ts->src/core/database/index.ts - + src/routes/docker-stats.ts->src/core/utils/helpers.ts - + src/routes/docker-stats.ts->src/core/docker/client.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/calculations.ts - + src/routes/docker-stats.ts->~/typings/dockerode - + src/routes/docker-stats.ts->src/core/utils/response-handler.ts - + src/routes/docker-websocket.ts->src/core/utils/logger.ts - + src/routes/docker-websocket.ts->src/core/database/index.ts - + src/routes/docker-websocket.ts->src/core/docker/client.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/calculations.ts - + src/routes/docker-websocket.ts->src/core/utils/response-handler.ts - + stream - + stream - + src/routes/docker-websocket.ts->stream - + src/routes/logs.ts->src/core/utils/logger.ts - + src/routes/logs.ts->src/core/database/index.ts - + src/routes/stacks.ts->src/core/utils/logger.ts - + src/routes/stacks.ts->src/core/database/index.ts - + src/routes/stacks.ts->src/core/stacks/controller.ts - - + + - + src/routes/stacks.ts->src/core/utils/response-handler.ts - + src/routes/utils.ts->src/core/utils/package-json.ts - + src/routes/utils.ts->src/core/utils/response-handler.ts From ab4968424bb79df4b83235b241b852d6b111d392 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 6 May 2025 20:20:14 +0200 Subject: [PATCH 297/324] CI/CD: Always publish test report --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f1c83b2..3047c17 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,6 +56,7 @@ jobs: bun clean - name: Publish Test Report + if: always() uses: mikepenz/action-junit-report@v5 with: report_paths: "unit-test.xml" From 64a9b9c61b87a00f2a0d7e789394bc5a9e51057e Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 8 May 2025 22:23:52 +0200 Subject: [PATCH 298/324] Feat: Bunch of changes, see future note --- biome.json | 51 +- docker/docker-compose.dev.yaml | 2 +- package.json | 19 +- src/core/database/logs.ts | 6 +- src/core/utils/calculations.ts | 2 +- src/index.ts | 251 +++---- src/routes/api-config.ts | 1143 ++++++++++++++++---------------- src/routes/docker-manager.ts | 3 +- src/routes/docker-stats.ts | 1028 +++++++++++++++------------- src/routes/logs.ts | 10 +- src/tests/api-config.spec.ts | 270 ++++++++ src/tests/cleanup.ts | 21 - src/tests/delete.spec.ts | 13 - src/tests/docker-manager.ts | 327 +++++++++ src/tests/gets.spec.ts | 61 -- src/tests/helper.ts | 129 ---- src/tests/junit-exporter.ts | 79 +++ src/tests/post.spec.ts | 52 -- 18 files changed, 1989 insertions(+), 1478 deletions(-) create mode 100644 src/tests/api-config.spec.ts delete mode 100644 src/tests/cleanup.ts delete mode 100644 src/tests/delete.spec.ts create mode 100644 src/tests/docker-manager.ts delete mode 100644 src/tests/gets.spec.ts delete mode 100644 src/tests/helper.ts create mode 100644 src/tests/junit-exporter.ts delete mode 100644 src/tests/post.spec.ts diff --git a/biome.json b/biome.json index bdb5ecc..f9d8224 100644 --- a/biome.json +++ b/biome.json @@ -1,26 +1,29 @@ { - "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", - "vcs": { - "enabled": true, - "clientKind": "git", - "useIgnoreFile": true - }, - "formatter": { - "enabled": true, - "indentStyle": "tab" - }, - "organizeImports": { - "enabled": true - }, - "linter": { - "enabled": true, - "rules": { - "recommended": true - } - }, - "javascript": { - "formatter": { - "quoteStyle": "double" - } - } + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "formatter": { + "enabled": true, + "indentStyle": "tab" + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + }, + "files": { + "ignore": ["./src/tests/junit-exporter.ts"] + } } diff --git a/docker/docker-compose.dev.yaml b/docker/docker-compose.dev.yaml index 878d1f0..799ae7a 100644 --- a/docker/docker-compose.dev.yaml +++ b/docker/docker-compose.dev.yaml @@ -47,6 +47,6 @@ services: ports: - 8080:8080 volumes: - - ../:/data:ro + - ../data:/data:ro environment: - SQLITE_DATABASE=dockstatapi.db diff --git a/package.json b/package.json index 3217c54..8fb4fe5 100644 --- a/package.json +++ b/package.json @@ -18,17 +18,18 @@ "build:prod": "NODE_ENV=production bun build --no-native --compile --minify-whitespace --minify-syntax --target bun --outfile server ./src/index.ts", "build:docker": "docker build -f docker/Dockerfile . -t 'dockstatapi:local'", "clean": "bun run clean:win || bun run clean:lin", - "clean:win": "node -e \"process.exit(process.platform === 'win32' ? 0 : 1)\" && cmd /c del /Q data/dockstatapi*.db* && echo 'success'", - "clean:lin": "node -e \"process.exit(process.platform !== 'win32' ? 0 : 1)\" && rm -f data/dockstatapi*.db* && echo 'success'", + "clean:win": "node -e \"process.exit(process.platform === 'win32' ? 0 : 1)\" && cmd /c del /Q data/dockstatapi* && cmd /c del /Q reports/junit/*.xml && echo 'success'", + "clean:lin": "node -e \"process.exit(process.platform !== 'win32' ? 0 : 1)\" && rm -f data/dockstatapi* && rm -f reports/junit/*.xml && echo 'success'", "knip": "knip", "lint": "biome check --formatter-enabled=true --linter-enabled=true --organize-imports-enabled=true --fix src", "test": "bun test src/tests/**/*.test.ts" }, "dependencies": { - "@elysiajs/server-timing": "^1.2.1", - "@elysiajs/static": "^1.2.0", - "@elysiajs/swagger": "^1.2.2", + "@elysiajs/server-timing": "^1.3.0", + "@elysiajs/static": "^1.3.0", + "@elysiajs/swagger": "^1.3.0", "chalk": "^5.4.1", + "date-fns": "^4.1.0", "docker-compose": "^1.2.0", "dockerode": "^4.0.6", "elysia": "latest", @@ -40,15 +41,15 @@ }, "devDependencies": { "@biomejs/biome": "1.9.4", + "@types/bun": "latest", "@types/dockerode": "^3.3.38", - "@types/node": "^22.15.3", + "@types/node": "^22.15.17", "@types/split2": "^4.2.3", "bun-types": "latest", "cross-env": "^7.0.3", "logform": "^2.7.0", "typescript": "^5.8.3", - "wrap-ansi": "^9.0.0", - "@types/bun": "latest" + "wrap-ansi": "^9.0.0" }, "module": "src/index.js", "trustedDependencies": [ @@ -56,4 +57,4 @@ ], "type": "module", "private": true -} \ No newline at end of file +} diff --git a/src/core/database/logs.ts b/src/core/database/logs.ts index f6b9980..eb815d5 100644 --- a/src/core/database/logs.ts +++ b/src/core/database/logs.ts @@ -4,13 +4,13 @@ import { executeDbOperation } from "./helper"; const stmt = { insert: db.prepare( - "INSERT INTO backend_log_entries (timestamp, level, message, file, line) VALUES (?, ?, ?, ?, ?)", + "INSERT INTO backend_log_entries (level, timestamp, message, file, line) VALUES (?, ?, ?, ?, ?)", ), selectAll: db.prepare( - "SELECT timestamp, level, message, file, line FROM backend_log_entries ORDER BY timestamp DESC", + "SELECT level, timestamp, message, file, line FROM backend_log_entries ORDER BY timestamp DESC", ), selectByLevel: db.prepare( - "SELECT timestamp, level, message, file, line FROM backend_log_entries WHERE level = ?", + "SELECT level, timestamp, message, file, line FROM backend_log_entries WHERE level = ?", ), deleteAll: db.prepare("DELETE FROM backend_log_entries"), deleteByLevel: db.prepare("DELETE FROM backend_log_entries WHERE level = ?"), diff --git a/src/core/utils/calculations.ts b/src/core/utils/calculations.ts index b640c47..fbb7a42 100644 --- a/src/core/utils/calculations.ts +++ b/src/core/utils/calculations.ts @@ -31,7 +31,7 @@ const calculateMemoryUsage = (stats: Docker.ContainerStats): number => { const data = (stats.memory_stats.usage / stats.memory_stats.limit) * 100; - return data ; + return data; }; export { calculateCpuPercent, calculateMemoryUsage }; diff --git a/src/index.ts b/src/index.ts index e2d478e..419c6bf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ import { serverTiming } from "@elysiajs/server-timing"; import staticPlugin from "@elysiajs/static"; import { swagger } from "@elysiajs/swagger"; -import { Elysia, t } from "elysia"; +import { Elysia } from "elysia"; import { dts } from "elysia-remote-dts"; import { dbFunctions } from "~/core/database"; import { monitorDockerEvents } from "~/core/docker/monitor"; @@ -9,9 +9,9 @@ import { setSchedules } from "~/core/docker/scheduler"; import { loadPlugins } from "~/core/plugins/loader"; import { logger } from "~/core/utils/logger"; import { - authorWebsite, - contributors, - license, + authorWebsite, + contributors, + license, } from "~/core/utils/package-json"; import { swaggerReadme } from "~/core/utils/swagger-readme"; import { validateApiKey } from "~/middleware/auth"; @@ -31,137 +31,140 @@ console.log(""); logger.info("Starting DockStatAPI"); const DockStatAPI = new Elysia() - .use(staticPlugin()) - .use(serverTiming()) - .use( - dts("./src/index.ts", { - tsconfig: "./tsconfig.json", - compilerOptions: { - strict: true, - }, - }) - ) - .use( - swagger({ - documentation: { - info: { - title: "DockStatAPI", - version: "3.0.0", - description: swaggerReadme, - }, - components: { - securitySchemes: { - apiKeyAuth: { - type: "apiKey" as const, - name: "x-api-key", - in: "header", - description: "API key for authentication", - }, - }, - }, - security: [ - { - apiKeyAuth: [], - }, - ], - tags: [ - { - name: "Statistics", - description: - "All endpoints for fetching statistics of hosts / containers", - }, - { - name: "Management", - description: "Various endpoints for managing DockStatAPI", - }, - { - name: "Stacks", - description: "DockStat's Stack functionality", - }, - { - name: "Utils", - description: "Various utilities which might be useful", - }, - ], - }, - }) - ) - .onBeforeHandle(async (context) => { - const { path, request, set } = context; + .use(staticPlugin()) + .use(serverTiming()) + .use( + dts("./src/index.ts", { + tsconfig: "./tsconfig.json", + compilerOptions: { + strict: true, + }, + }), + ) + .use( + swagger({ + documentation: { + info: { + title: "DockStatAPI", + version: "3.0.0", + description: swaggerReadme, + }, + components: { + securitySchemes: { + apiKeyAuth: { + type: "apiKey" as const, + name: "x-api-key", + in: "header", + description: "API key for authentication", + }, + }, + }, + security: [ + { + apiKeyAuth: [], + }, + ], + tags: [ + { + name: "Statistics", + description: + "All endpoints for fetching statistics of hosts / containers", + }, + { + name: "Management", + description: "Various endpoints for managing DockStatAPI", + }, + { + name: "Stacks", + description: "DockStat's Stack functionality", + }, + { + name: "Utils", + description: "Various utilities which might be useful", + }, + ], + }, + }), + ) + .onBeforeHandle(async (context) => { + const { path, request, set } = context; - if ( - path === "/health" || - path.startsWith("/swagger") || - path.startsWith("/trpc") - ) { - logger.info(`Requested unguarded route: ${path}`); - return; - } + if ( + path === "/health" || + path.startsWith("/swagger") || + path.startsWith("/trpc") + ) { + logger.info(`Requested unguarded route: ${path}`); + return; + } - const validation = await validateApiKey(request, set); + const validation = await validateApiKey(request, set); - if (validation.error) { - set.status = 400; - set.headers["Content-Type"] = "application/json"; - return { error: validation.error }; - } - }) - .onError(({ code, set, path }) => { - if (code === "NOT_FOUND") { - logger.warn(`Unknown route (${path}), showing error page!`); - set.status = 404; - set.headers["Content-Type"] = "text/html"; - return Bun.file("public/404.html"); - } - }) - .use(dockerRoutes) - .use(dockerStatsRoutes) - .use(backendLogs) - .use(dockerWebsocketRoutes) - .use(apiConfigRoutes) - .use(utilRoutes) - .use(stackRoutes) - .use(liveLogs) - .use(liveStacks) - .get("/health", () => ({ status: "healthy" }), { tags: ["Utils"] }) - .listen(process.env.DOCKSTATAPI_PORT || 3000, ({ hostname, port }) => { - console.log("----- [ ############## ]"); - logger.info(`DockStatAPI is running at http://${hostname}:${port}`); - logger.info( - `Swagger API Documentation available at http://${hostname}:${port}/swagger` - ); - logger.info(`License: ${license}`); - logger.info(`Author: ${authorWebsite}`); - logger.info(`Contributors: ${contributors}`); - }); + if (validation.error) { + set.status = 400; + + return { error: validation.error }; + } + }) + .onError(({ code, set, path }) => { + if (code === "NOT_FOUND") { + logger.warn(`Unknown route (${path}), showing error page!`); + set.status = 404; + set.headers["Content-Type"] = "text/html"; + return Bun.file("public/404.html"); + } + }) + .use(dockerRoutes) + .use(dockerStatsRoutes) + .use(backendLogs) + .use(dockerWebsocketRoutes) + .use(apiConfigRoutes) + .use(utilRoutes) + .use(stackRoutes) + .use(liveLogs) + .use(liveStacks) + .get("/health", () => ({ status: "healthy" }), { + tags: ["Utils"], + response: { message: "healthy" }, + }) + .listen(process.env.DOCKSTATAPI_PORT || 3000, ({ hostname, port }) => { + console.log("----- [ ############## ]"); + logger.info(`DockStatAPI is running at http://${hostname}:${port}`); + logger.info( + `Swagger API Documentation available at http://${hostname}:${port}/swagger`, + ); + logger.info(`License: ${license}`); + logger.info(`Author: ${authorWebsite}`); + logger.info(`Contributors: ${contributors}`); + }); const initializeServer = async () => { - try { - await loadPlugins("./src/plugins"); - await setSchedules(); + try { + await loadPlugins("./src/plugins"); + await setSchedules(); - monitorDockerEvents().catch((error) => { - logger.error(`Monitoring Error: ${error}`); - }); + monitorDockerEvents().catch((error) => { + logger.error(`Monitoring Error: ${error}`); + }); - const configData = dbFunctions.getConfig() as config[]; - const apiKey = configData[0].api_key; + const configData = dbFunctions.getConfig() as config[]; + const apiKey = configData[0].api_key; - if (apiKey === "changeme") { - logger.warn( - "Default API Key of 'changeme' detected. Please change your API Key via the `/config/update` route!" - ); - } + if (apiKey === "changeme") { + logger.warn( + "Default API Key of 'changeme' detected. Please change your API Key via the `/config/update` route!", + ); + } - logger.info("Started server"); - console.log("----- [ ############## ]"); - } catch (error) { - logger.error("Error while starting server:", error); - process.exit(1); - } + logger.info("Started server"); + console.log("----- [ ############## ]"); + } catch (error) { + logger.error("Error while starting server:", error); + process.exit(1); + } }; await initializeServer(); -export { DockStatAPI }; export type App = typeof DockStatAPI; +export { DockStatAPI }; diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index 79749e6..7e51d8b 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -4,15 +4,15 @@ import { dbFunctions } from "~/core/database"; import { pluginManager } from "~/core/plugins/plugin-manager"; import { logger } from "~/core/utils/logger"; import { - authorEmail, - authorName, - authorWebsite, - contributors, - dependencies, - description, - devDependencies, - license, - version, + authorEmail, + authorName, + authorWebsite, + contributors, + dependencies, + description, + devDependencies, + license, + version, } from "~/core/utils/package-json"; import { responseHandler } from "~/core/utils/response-handler"; @@ -21,577 +21,576 @@ import { hashApiKey } from "~/middleware/auth"; import type { config } from "~/typings/database"; export const apiConfigRoutes = new Elysia({ prefix: "/config" }) - .get( - "/", - async ({ set }) => { - try { - const data = dbFunctions.getConfig() as config[]; - const distinct = data[0]; - set.status = 200; - set.headers["Content-Type"] = "application/json"; - logger.debug("Fetched backend config"); - return distinct; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Error getting the DockStatAPI config" - ); - } - }, - { - detail: { - tags: ["Management"], - description: - "Returns current API configuration including data retention policies and security settings", - responses: { - "200": { - description: "Successfully retrieved configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - fetching_interval: { - type: "number", - example: 5, - }, - keep_data_for: { - type: "number", - example: 7, - }, - api_key: { - type: "string", - example: "hashed_api_key", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error getting the DockStatAPI config", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - .get( - "/plugins", - ({ set }) => { - try { - return pluginManager.getLoadedPlugins(); - } catch (error) { - return responseHandler.error( - set, - error as string, - "Error getting all registered plugins" - ); - } - }, - { - detail: { - tags: ["Management"], - description: - "Lists all active plugins with their registration details and status", - responses: { - "200": { - description: "Successfully retrieved plugins", - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "object", - properties: { - name: { - type: "string", - example: "example-plugin", - }, - version: { - type: "string", - example: "1.0.0", - }, - status: { - type: "string", - example: "active", - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving plugins", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error getting all registered plugins", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - .post( - "/update", - async ({ set, body }) => { - try { - const { fetching_interval, keep_data_for, api_key } = body; - set.headers["Content-Type"] = "application/json"; - dbFunctions.updateConfig( - fetching_interval, - keep_data_for, - await hashApiKey(api_key) - ); - return responseHandler.ok(set, "Updated DockStatAPI config"); - } catch (error) { - return responseHandler.error( - set, - "Error updating the DockStatAPI config", - error as string - ); - } - }, - { - detail: { - tags: ["Management"], - description: - "Modifies core API settings including data collection intervals, retention periods, and security credentials", - responses: { - "200": { - description: "Successfully updated configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Updated DockStatAPI config", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error updating configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error updating the DockStatAPI config", - }, - }, - }, - }, - }, - }, - }, - }, - body: t.Object({ - fetching_interval: t.Number(), - keep_data_for: t.Number(), - api_key: t.String(), - }), - } - ) - .get( - "/package", - async ({ set }) => { - try { - logger.debug("Fetching package.json"); - return { - version: version, - description: description, - license: license, - authorName: authorName, - authorEmail: authorEmail, - authorWebsite: authorWebsite, - contributors: contributors, - dependencies: dependencies, - devDependencies: devDependencies, - }; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Error while reading package.json" - ); - } - }, - { - detail: { - tags: ["Management"], - description: - "Displays package metadata including dependencies, contributors, and licensing information", - responses: { - "200": { - description: "Successfully retrieved package information", - content: { - "application/json": { - schema: { - type: "object", - properties: { - version: { - type: "string", - example: "3.0.0", - }, - description: { - type: "string", - example: - "DockStatAPI is an API backend featuring plugins and more for DockStat", - }, - license: { - type: "string", - example: "CC BY-NC 4.0", - }, - authorName: { - type: "string", - example: "ItsNik", - }, - authorEmail: { - type: "string", - example: "info@itsnik.de", - }, - authorWebsite: { - type: "string", - example: "https://github.com/Its4Nik", - }, - contributors: { - type: "array", - items: { - type: "string", - }, - example: [], - }, - dependencies: { - type: "object", - example: { - "@elysiajs/server-timing": "^1.2.1", - "@elysiajs/static": "^1.2.0", - }, - }, - devDependencies: { - type: "object", - example: { - "@biomejs/biome": "1.9.4", - "@types/dockerode": "^3.3.38", - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving package information", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error while reading package.json", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - .post( - "/backup", - async ({ set }) => { - try { - const backupFilename = await dbFunctions.backupDatabase(); - return responseHandler.ok(set, backupFilename); - } catch (error) { - return responseHandler.error(set, error as string, "Error backing up"); - } - }, - { - detail: { - tags: ["Management"], - description: "Backs up the internal database", - responses: { - "200": { - description: "Successfully created backup", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "backup_2024-03-20_12-00-00.db.bak", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error creating backup", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error backing up", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - .get( - "/backup", - async ({ set }) => { - try { - const backupFiles = readdirSync(backupDir); + .get( + "", + async ({ set }) => { + try { + const data = dbFunctions.getConfig() as config[]; + const distinct = data[0]; + set.status = 200; - const filteredFiles = backupFiles.filter((file: string) => { - return !( - file.endsWith(".db") || - file.endsWith(".db-shm") || - file.endsWith(".db-wal") - ); - }); + logger.debug("Fetched backend config"); + return distinct; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Error getting the DockStatAPI config", + ); + } + }, + { + detail: { + tags: ["Management"], + description: + "Returns current API configuration including data retention policies and security settings", + responses: { + "200": { + description: "Successfully retrieved configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + fetching_interval: { + type: "number", + example: 5, + }, + keep_data_for: { + type: "number", + example: 7, + }, + api_key: { + type: "string", + example: "hashed_api_key", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error getting the DockStatAPI config", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) + .get( + "/plugins", + ({ set }) => { + try { + return pluginManager.getLoadedPlugins(); + } catch (error) { + return responseHandler.error( + set, + error as string, + "Error getting all registered plugins", + ); + } + }, + { + detail: { + tags: ["Management"], + description: + "Lists all active plugins with their registration details and status", + responses: { + "200": { + description: "Successfully retrieved plugins", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "object", + properties: { + name: { + type: "string", + example: "example-plugin", + }, + version: { + type: "string", + example: "1.0.0", + }, + status: { + type: "string", + example: "active", + }, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving plugins", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error getting all registered plugins", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) + .post( + "/update", + async ({ set, body }) => { + try { + const { fetching_interval, keep_data_for, api_key } = body; - return filteredFiles; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Reading Backup directory" - ); - } - }, - { - detail: { - tags: ["Management"], - description: "Lists all available backups", - responses: { - "200": { - description: "Successfully retrieved backup list", - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "string", - }, - example: [ - "backup_2024-03-20_12-00-00.db.bak", - "backup_2024-03-19_12-00-00.db.bak", - ], - }, - }, - }, - }, - "400": { - description: "Error retrieving backup list", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Reading Backup directory", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) + dbFunctions.updateConfig( + fetching_interval, + keep_data_for, + await hashApiKey(api_key), + ); + return responseHandler.ok(set, "Updated DockStatAPI config"); + } catch (error) { + return responseHandler.error( + set, + "Error updating the DockStatAPI config", + error as string, + ); + } + }, + { + detail: { + tags: ["Management"], + description: + "Modifies core API settings including data collection intervals, retention periods, and security credentials", + responses: { + "200": { + description: "Successfully updated configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Updated DockStatAPI config", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error updating configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error updating the DockStatAPI config", + }, + }, + }, + }, + }, + }, + }, + }, + body: t.Object({ + fetching_interval: t.Number(), + keep_data_for: t.Number(), + api_key: t.String(), + }), + }, + ) + .get( + "/package", + async ({ set }) => { + try { + logger.debug("Fetching package.json"); + return { + version: version, + description: description, + license: license, + authorName: authorName, + authorEmail: authorEmail, + authorWebsite: authorWebsite, + contributors: contributors, + dependencies: dependencies, + devDependencies: devDependencies, + }; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Error while reading package.json", + ); + } + }, + { + detail: { + tags: ["Management"], + description: + "Displays package metadata including dependencies, contributors, and licensing information", + responses: { + "200": { + description: "Successfully retrieved package information", + content: { + "application/json": { + schema: { + type: "object", + properties: { + version: { + type: "string", + example: "3.0.0", + }, + description: { + type: "string", + example: + "DockStatAPI is an API backend featuring plugins and more for DockStat", + }, + license: { + type: "string", + example: "CC BY-NC 4.0", + }, + authorName: { + type: "string", + example: "ItsNik", + }, + authorEmail: { + type: "string", + example: "info@itsnik.de", + }, + authorWebsite: { + type: "string", + example: "https://github.com/Its4Nik", + }, + contributors: { + type: "array", + items: { + type: "string", + }, + example: [], + }, + dependencies: { + type: "object", + example: { + "@elysiajs/server-timing": "^1.2.1", + "@elysiajs/static": "^1.2.0", + }, + }, + devDependencies: { + type: "object", + example: { + "@biomejs/biome": "1.9.4", + "@types/dockerode": "^3.3.38", + }, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving package information", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error while reading package.json", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) + .post( + "/backup", + async ({ set }) => { + try { + const backupFilename = await dbFunctions.backupDatabase(); + return responseHandler.ok(set, backupFilename); + } catch (error) { + return responseHandler.error(set, error as string, "Error backing up"); + } + }, + { + detail: { + tags: ["Management"], + description: "Backs up the internal database", + responses: { + "200": { + description: "Successfully created backup", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "backup_2024-03-20_12-00-00.db.bak", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error creating backup", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error backing up", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) + .get( + "/backup", + async ({ set }) => { + try { + const backupFiles = readdirSync(backupDir); - .get( - "/backup/download", - async ({ query, set }) => { - try { - const filename = query.filename || dbFunctions.findLatestBackup(); - const filePath = `${backupDir}/${filename}`; + const filteredFiles = backupFiles.filter((file: string) => { + return !( + file.endsWith(".db") || + file.endsWith(".db-shm") || + file.endsWith(".db-wal") + ); + }); - if (!existsSync(filePath)) { - throw new Error("Backup file not found"); - } + return filteredFiles; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Reading Backup directory", + ); + } + }, + { + detail: { + tags: ["Management"], + description: "Lists all available backups", + responses: { + "200": { + description: "Successfully retrieved backup list", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "string", + }, + example: [ + "backup_2024-03-20_12-00-00.db.bak", + "backup_2024-03-19_12-00-00.db.bak", + ], + }, + }, + }, + }, + "400": { + description: "Error retrieving backup list", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Reading Backup directory", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) - set.headers["Content-Type"] = "application/octet-stream"; - set.headers[ - "Content-Disposition" - ] = `attachment; filename="${filename}"`; - return Bun.file(filePath); - } catch (error) { - return responseHandler.error( - set, - error as string, - "Backup download failed" - ); - } - }, - { - detail: { - tags: ["Management"], - description: - "Download a specific backup or the latest if no filename is provided", - responses: { - "200": { - description: "Successfully downloaded backup file", - content: { - "application/octet-stream": { - schema: { - type: "string", - format: "binary", - example: "Binary backup file content", - }, - }, - }, - headers: { - "Content-Disposition": { - schema: { - type: "string", - example: - 'attachment; filename="backup_2024-03-20_12-00-00.db.bak"', - }, - }, - }, - }, - "400": { - description: "Error downloading backup", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Backup download failed", - }, - }, - }, - }, - }, - }, - }, - }, - query: t.Object({ - filename: t.Optional(t.String()), - }), - } - ) - .post( - "/restore", - async ({ body, set }) => { - try { - const { file } = body; + .get( + "/backup/download", + async ({ query, set }) => { + try { + const filename = query.filename || dbFunctions.findLatestBackup(); + const filePath = `${backupDir}/${filename}`; - set.headers["Content-Type"] = "text/html"; + if (!existsSync(filePath)) { + throw new Error("Backup file not found"); + } - if (!file) { - throw new Error("No file uploaded"); - } + set.headers["Content-Type"] = "application/octet-stream"; + set.headers["Content-Disposition"] = + `attachment; filename="${filename}"`; + return Bun.file(filePath); + } catch (error) { + return responseHandler.error( + set, + error as string, + "Backup download failed", + ); + } + }, + { + detail: { + tags: ["Management"], + description: + "Download a specific backup or the latest if no filename is provided", + responses: { + "200": { + description: "Successfully downloaded backup file", + content: { + "application/octet-stream": { + schema: { + type: "string", + format: "binary", + example: "Binary backup file content", + }, + }, + }, + headers: { + "Content-Disposition": { + schema: { + type: "string", + example: + 'attachment; filename="backup_2024-03-20_12-00-00.db.bak"', + }, + }, + }, + }, + "400": { + description: "Error downloading backup", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Backup download failed", + }, + }, + }, + }, + }, + }, + }, + }, + query: t.Object({ + filename: t.Optional(t.String()), + }), + }, + ) + .post( + "/restore", + async ({ body, set }) => { + try { + const { file } = body; - if (!file.name.endsWith(".db.bak")) { - throw new Error("Invalid file type. Expected .db.bak"); - } + set.headers["Content-Type"] = "text/html"; - const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; - const fileBuffer = await file.arrayBuffer(); + if (!file) { + throw new Error("No file uploaded"); + } - await Bun.write(tempPath, fileBuffer); - dbFunctions.restoreDatabase(tempPath); - unlinkSync(tempPath); + if (!file.name.endsWith(".db.bak")) { + throw new Error("Invalid file type. Expected .db.bak"); + } - return responseHandler.ok(set, "Database restored successfully"); - } catch (error) { - return responseHandler.error( - set, - error instanceof Error ? error.message : "Restoration failed", - "Database restoration error" - ); - } - }, - { - body: t.Object({ file: t.File() }), - detail: { - tags: ["Management"], - description: "Restore database from uploaded backup file", - responses: { - "200": { - description: "Successfully restored database", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Database restored successfully", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error restoring database", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Database restoration error", - }, - }, - }, - }, - }, - }, - }, - }, - } - ); + const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; + const fileBuffer = await file.arrayBuffer(); + + await Bun.write(tempPath, fileBuffer); + dbFunctions.restoreDatabase(tempPath); + unlinkSync(tempPath); + + return responseHandler.ok(set, "Database restored successfully"); + } catch (error) { + return responseHandler.error( + set, + error instanceof Error ? error.message : "Restoration failed", + "Database restoration error", + ); + } + }, + { + body: t.Object({ file: t.File() }), + detail: { + tags: ["Management"], + description: "Restore database from uploaded backup file", + responses: { + "200": { + description: "Successfully restored database", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Database restored successfully", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error restoring database", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Database restoration error", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ); diff --git a/src/routes/docker-manager.ts b/src/routes/docker-manager.ts index 68044cd..30cd5c4 100644 --- a/src/routes/docker-manager.ts +++ b/src/routes/docker-manager.ts @@ -11,7 +11,6 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) "/add-host", async ({ set, body }) => { try { - set.headers["Content-Type"] = "application/json"; dbFunctions.addDockerHost(body as DockerHost); return responseHandler.ok(set, `Added docker host (${body.name})`); } catch (error: unknown) { @@ -139,7 +138,7 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) async ({ set }) => { try { const dockerHosts = dbFunctions.getDockerHosts(); - set.headers["Content-Type"] = "application/json"; + logger.debug("Retrieved docker hosts"); return dockerHosts; } catch (error) { diff --git a/src/routes/docker-stats.ts b/src/routes/docker-stats.ts index 42b887d..b411f2d 100644 --- a/src/routes/docker-stats.ts +++ b/src/routes/docker-stats.ts @@ -4,8 +4,8 @@ import { Elysia } from "elysia"; import { dbFunctions } from "~/core/database"; import { getDockerClient } from "~/core/docker/client"; import { - calculateCpuPercent, - calculateMemoryUsage, + calculateCpuPercent, + calculateMemoryUsage, } from "~/core/utils/calculations"; import { findObjectByKey } from "~/core/utils/helpers"; import { logger } from "~/core/utils/logger"; @@ -15,481 +15,587 @@ import type { ContainerInfo, DockerHost, HostStats } from "~/typings/docker"; import type { DockerInfo } from "~/typings/dockerode"; export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) - .get( - "/containers", - async ({ set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const containers: ContainerInfo[] = []; + .get( + "/containers", + async ({ set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + const containers: ContainerInfo[] = []; - await Promise.all( - hosts.map(async (host) => { - try { - const docker = getDockerClient(host); - try { - await docker.ping(); - } catch (pingError) { - return responseHandler.error( - set, - pingError as string, - "Docker host connection failed" - ); - } + await Promise.all( + hosts.map(async (host) => { + try { + const docker = getDockerClient(host); + try { + await docker.ping(); + } catch (pingError) { + return responseHandler.error( + set, + pingError as string, + "Docker host connection failed", + ); + } - const hostContainers = await docker.listContainers({ all: true }); + const hostContainers = await docker.listContainers({ all: true }); - await Promise.all( - hostContainers.map(async (containerInfo) => { - try { - const container = docker.getContainer(containerInfo.Id); - const stats = await new Promise( - (resolve, reject) => { - container.stats({ stream: false }, (error, stats) => { - if (error) { - return responseHandler.reject( - set, - reject, - "An error occurred", - error - ); - } - if (!stats) { - return responseHandler.reject( - set, - reject, - "No stats available" - ); - } - resolve(stats); - }); - } - ); + await Promise.all( + hostContainers.map(async (containerInfo) => { + try { + const container = docker.getContainer(containerInfo.Id); + const stats = await new Promise( + (resolve, reject) => { + container.stats({ stream: false }, (error, stats) => { + if (error) { + return responseHandler.reject( + set, + reject, + "An error occurred", + error, + ); + } + if (!stats) { + return responseHandler.reject( + set, + reject, + "No stats available", + ); + } + resolve(stats); + }); + }, + ); - containers.push({ - id: containerInfo.Id, - hostId: `${host.id}`, - name: containerInfo.Names[0].replace(/^\//, ""), - image: containerInfo.Image, - status: containerInfo.Status, - state: containerInfo.State, - cpuUsage: calculateCpuPercent(stats), - memoryUsage: calculateMemoryUsage(stats), - stats: stats, - info: containerInfo, - }); - } catch (containerError) { - logger.error( - "Error fetching container stats,", - containerError - ); - } - }) - ); - logger.debug(`Fetched stats for ${host.name}`); - } catch (hostError) { - logger.error("Error fetching containers for host,", hostError); - } - }) - ); + containers.push({ + id: containerInfo.Id, + hostId: `${host.id}`, + name: containerInfo.Names[0].replace(/^\//, ""), + image: containerInfo.Image, + status: containerInfo.Status, + state: containerInfo.State, + cpuUsage: calculateCpuPercent(stats), + memoryUsage: calculateMemoryUsage(stats), + stats: stats, + info: containerInfo, + }); + } catch (containerError) { + logger.error( + "Error fetching container stats,", + containerError, + ); + } + }), + ); + logger.debug(`Fetched stats for ${host.name}`); + } catch (hostError) { + logger.error("Error fetching containers for host,", hostError); + } + }), + ); - set.headers["Content-Type"] = "application/json"; - logger.debug("Fetched all containers across all hosts"); - return { containers }; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to retrieve containers" - ); - } - }, - { - detail: { - tags: ["Statistics"], - description: - "Collects real-time statistics for all Docker containers across monitored hosts, including CPU and memory utilization", - responses: { - "200": { - description: "Successfully retrieved container statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - containers: { - type: "array", - items: { - type: "object", - properties: { - id: { - type: "string", - example: "abc123def456", - }, - hostId: { - type: "string", - example: "1", - }, - name: { - type: "string", - example: "example-container", - }, - image: { - type: "string", - example: "nginx:latest", - }, - status: { - type: "string", - example: "running", - }, - state: { - type: "string", - example: "running", - }, - cpuUsage: { - type: "number", - example: 0.5, - }, - memoryUsage: { - type: "number", - example: 1024, - }, - }, - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving container statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve containers", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - .get( - "/hosts", - async ({ set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + logger.debug("Fetched all containers across all hosts"); + return { containers }; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to retrieve containers", + ); + } + }, + { + detail: { + tags: ["Statistics"], + description: + "Collects real-time statistics for all Docker containers across monitored hosts, including CPU and memory utilization", + responses: { + "200": { + description: "Successfully retrieved container statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + containers: { + type: "array", + items: { + type: "object", + properties: { + id: { + type: "string", + example: "abc123def456", + }, + hostId: { + type: "string", + example: "1", + }, + name: { + type: "string", + example: "example-container", + }, + image: { + type: "string", + example: "nginx:latest", + }, + status: { + type: "string", + example: "running", + }, + state: { + type: "string", + example: "running", + }, + cpuUsage: { + type: "number", + example: 0.5, + }, + memoryUsage: { + type: "number", + example: 1024, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving container statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve containers", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) + .get( + "/hosts", + async ({ set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const stats: HostStats[] = []; + const stats: HostStats[] = []; - for (const host of hosts) { - const docker = getDockerClient(host); - const info: DockerInfo = await docker.info(); + for (const host of hosts) { + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); - const config: HostStats = { - hostId: host.id as number, - hostName: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - labels: info.Labels, - images: info.Images, - containers: info.Containers, - containersPaused: info.ContainersPaused, - containersRunning: info.ContainersRunning, - containersStopped: info.ContainersStopped, - }; + const config: HostStats = { + hostId: host.id as number, + hostName: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; - stats.push(config); - } + stats.push(config); + } - set.headers["Content-Type"] = "application/json"; - logger.debug("Fetched all hosts"); - return stats; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to retrieve host config" - ); - } - }, - { - detail: { - tags: ["Statistics"], - description: - "Provides detailed system metrics and Docker runtime information for specified host", - responses: { - "200": { - description: "Successfully retrieved host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - hostId: { - type: "number", - example: 1, - }, - hostName: { - type: "string", - example: "Localhost", - }, - dockerVersion: { - type: "string", - example: "24.0.5", - }, - apiVersion: { - type: "string", - example: "1.41", - }, - os: { - type: "string", - example: "Linux", - }, - architecture: { - type: "string", - example: "x86_64", - }, - totalMemory: { - type: "number", - example: 16777216, - }, - totalCPU: { - type: "number", - example: 4, - }, - labels: { - type: "array", - items: { - type: "string", - }, - example: ["environment=production"], - }, - images: { - type: "number", - example: 10, - }, - containers: { - type: "number", - example: 5, - }, - containersPaused: { - type: "number", - example: 0, - }, - containersRunning: { - type: "number", - example: 4, - }, - containersStopped: { - type: "number", - example: 1, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve host config", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - .get( - "/hosts/:id", - async ({ params, set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + logger.debug("Fetched all hosts"); + return stats; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to retrieve host config", + ); + } + }, + { + detail: { + tags: ["Statistics"], + description: + "Provides detailed system metrics and Docker runtime information for specified host", + responses: { + "200": { + description: "Successfully retrieved host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + hostId: { + type: "number", + example: 1, + }, + hostName: { + type: "string", + example: "Localhost", + }, + dockerVersion: { + type: "string", + example: "24.0.5", + }, + apiVersion: { + type: "string", + example: "1.41", + }, + os: { + type: "string", + example: "Linux", + }, + architecture: { + type: "string", + example: "x86_64", + }, + totalMemory: { + type: "number", + example: 16777216, + }, + totalCPU: { + type: "number", + example: 4, + }, + labels: { + type: "array", + items: { + type: "string", + }, + example: ["environment=production"], + }, + images: { + type: "number", + example: 10, + }, + containers: { + type: "number", + example: 5, + }, + containersPaused: { + type: "number", + example: 0, + }, + containersRunning: { + type: "number", + example: 4, + }, + containersStopped: { + type: "number", + example: 1, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve host config", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) + .get( + "/hosts", + async ({ set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - if (!params.id) { - const stats: HostStats[] = []; + const stats: HostStats[] = []; - for (const host of hosts) { - const docker = getDockerClient(host); - const info: DockerInfo = await docker.info(); + for (const host of hosts) { + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); - const config: HostStats = { - hostId: host.id as number, - hostName: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - labels: info.Labels, - images: info.Images, - containers: info.Containers, - containersPaused: info.ContainersPaused, - containersRunning: info.ContainersRunning, - containersStopped: info.ContainersStopped, - }; + const config: HostStats = { + hostId: host.id as number, + hostName: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; - stats.push(config); - } + stats.push(config); + } - return stats; - } + logger.debug("Fetched stats for all hosts"); + return stats; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to retrieve host config", + ); + } + }, + { + detail: { + tags: ["Statistics"], + description: + "Provides detailed system metrics and Docker runtime information for all hosts", + responses: { + "200": { + description: "Successfully retrieved host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + hostId: { + type: "number", + example: 1, + }, + hostName: { + type: "string", + example: "Localhost", + }, + dockerVersion: { + type: "string", + example: "24.0.5", + }, + apiVersion: { + type: "string", + example: "1.41", + }, + os: { + type: "string", + example: "Linux", + }, + architecture: { + type: "string", + example: "x86_64", + }, + totalMemory: { + type: "number", + example: 16777216, + }, + totalCPU: { + type: "number", + example: 4, + }, + labels: { + type: "array", + items: { + type: "string", + }, + example: ["environment=production"], + }, + images: { + type: "number", + example: 10, + }, + containers: { + type: "number", + example: 5, + }, + containersPaused: { + type: "number", + example: 0, + }, + containersRunning: { + type: "number", + example: 4, + }, + containersStopped: { + type: "number", + example: 1, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve host config", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) + .get( + "/hosts/:id", + async ({ params, set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const host = findObjectByKey(hosts, "id", Number(params.id)); - if (!host) { - return responseHandler.simple_error( - set, - `Host (${params.id}) not found` - ); - } + const host = findObjectByKey(hosts, "id", Number(params.id)); + if (!host) { + return responseHandler.simple_error( + set, + `Host (${params.id}) not found`, + ); + } - const docker = getDockerClient(host); - const info: DockerInfo = await docker.info(); + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); - const config: HostStats = { - hostId: host.id as number, - hostName: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - labels: info.Labels, - images: info.Images, - containers: info.Containers, - containersPaused: info.ContainersPaused, - containersRunning: info.ContainersRunning, - containersStopped: info.ContainersStopped, - }; + const config: HostStats = { + hostId: host.id as number, + hostName: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; - set.headers["Content-Type"] = "application/json"; - logger.debug(`Fetched config for ${host.name}`); - return config; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to retrieve host config" - ); - } - }, - { - detail: { - tags: ["Statistics"], - description: - "Provides detailed system metrics and Docker runtime information for specified host", - responses: { - "200": { - description: "Successfully retrieved host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - hostId: { - type: "number", - example: 1, - }, - hostName: { - type: "string", - example: "Localhost", - }, - dockerVersion: { - type: "string", - example: "24.0.5", - }, - apiVersion: { - type: "string", - example: "1.41", - }, - os: { - type: "string", - example: "Linux", - }, - architecture: { - type: "string", - example: "x86_64", - }, - totalMemory: { - type: "number", - example: 16777216, - }, - totalCPU: { - type: "number", - example: 4, - }, - labels: { - type: "array", - items: { - type: "string", - }, - example: ["environment=production"], - }, - images: { - type: "number", - example: 10, - }, - containers: { - type: "number", - example: 5, - }, - containersPaused: { - type: "number", - example: 0, - }, - containersRunning: { - type: "number", - example: 4, - }, - containersStopped: { - type: "number", - example: 1, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve host config", - }, - }, - }, - }, - }, - }, - }, - }, - } - ); + logger.debug(`Fetched config for ${host.name}`); + return config; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to retrieve host config", + ); + } + }, + { + detail: { + tags: ["Statistics"], + description: + "Provides detailed system metrics and Docker runtime information for specified host", + responses: { + "200": { + description: "Successfully retrieved host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + hostId: { + type: "number", + example: 1, + }, + hostName: { + type: "string", + example: "Localhost", + }, + dockerVersion: { + type: "string", + example: "24.0.5", + }, + apiVersion: { + type: "string", + example: "1.41", + }, + os: { + type: "string", + example: "Linux", + }, + architecture: { + type: "string", + example: "x86_64", + }, + totalMemory: { + type: "number", + example: 16777216, + }, + totalCPU: { + type: "number", + example: 4, + }, + labels: { + type: "array", + items: { + type: "string", + }, + example: ["environment=production"], + }, + images: { + type: "number", + example: 10, + }, + containers: { + type: "number", + example: 5, + }, + containersPaused: { + type: "number", + example: 0, + }, + containersRunning: { + type: "number", + example: 4, + }, + containersStopped: { + type: "number", + example: 1, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve host config", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ); diff --git a/src/routes/logs.ts b/src/routes/logs.ts index a4feb15..17da1fb 100644 --- a/src/routes/logs.ts +++ b/src/routes/logs.ts @@ -5,11 +5,11 @@ import { logger } from "~/core/utils/logger"; export const backendLogs = new Elysia({ prefix: "/logs" }) .get( - "/", + "", async ({ set }) => { try { const logs = dbFunctions.getAllLogs(); - set.headers["Content-Type"] = "application/json"; + // logger.debug("Retrieved all logs"); return logs; } catch (error) { @@ -81,7 +81,7 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) async ({ params: { level }, set }) => { try { const logs = dbFunctions.getLogsByLevel(level); - set.headers["Content-Type"] = "application/json"; + logger.debug(`Retrieved logs (level: ${level})`); return logs; } catch (error) { @@ -153,7 +153,7 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) async ({ set }) => { try { set.status = 200; - set.headers["Content-Type"] = "application/json"; + dbFunctions.clearAllLogs(); return { success: true }; } catch (error) { @@ -209,7 +209,7 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) async ({ params: { level }, set }) => { try { dbFunctions.clearLogsByLevel(level); - set.headers["Content-Type"] = "application/json"; + logger.debug(`Cleared all logs with level: ${level}`); return { success: true }; } catch (error) { diff --git a/src/tests/api-config.spec.ts b/src/tests/api-config.spec.ts new file mode 100644 index 0000000..1241885 --- /dev/null +++ b/src/tests/api-config.spec.ts @@ -0,0 +1,270 @@ +import { afterAll, beforeEach, describe, expect, it, mock } from "bun:test"; +import { Elysia } from "elysia"; +import { logger } from "~/core/utils/logger"; +import { apiConfigRoutes } from "~/routes/api-config"; +import { + generateJunitReport, + recordTestResult, + testResults, +} from "./junit-exporter"; + +const mockDb = { + getConfig: mock(() => [ + { + fetching_interval: 10, + keep_data_for: 14, + api_key: "$argon2id$v=19$m=65536,t=2,p=1$...", + }, + ]), + updateConfig: mock(), + backupDatabase: mock( + () => `dockstatapi-${new Date().toISOString().slice(0, 10)}.db.bak`, + ), + restoreDatabase: mock(), + findLatestBackup: mock(() => "dockstatapi-2025-05-06.db.bak"), +}; + +mock.module("node:fs", () => ({ + existsSync: mock((path) => path.includes("dockstatapi")), + readdirSync: mock(() => [ + "dockstatapi-2025-05-06.db.bak", + "dockstatapi.db", + "dockstatapi.db-shm", + ]), + unlinkSync: mock(), +})); + +const mockPlugins = [ + { + name: "docker-monitor", + version: "1.2.0", + status: "active", + }, +]; + +const createTestApp = () => + new Elysia().use(apiConfigRoutes).decorate({ + db: mockDb, + pluginManager: { + getLoadedPlugins: mock(() => mockPlugins), + getPlugin: mock((name) => mockPlugins.find((p) => p.name === name)), + }, + logger: { + ...logger, + debug: mock(), + error: mock(), + info: mock(), + }, + }); + +describe("API Configuration Endpoints", () => { + beforeEach(() => { + mockDb.getConfig.mockClear(); + mockDb.updateConfig.mockClear(); + }); + + describe("Core Configuration", () => { + it("should retrieve current config with hashed API key", async () => { + const start = Date.now(); + try { + const app = createTestApp(); + const res = await app.handle( + new Request("http://localhost:3000/config"), + ); + + expect(res.status).toBe(200); + const data = await res.json(); + expect(data).toMatchObject({ + fetching_interval: expect.any(Number), + keep_data_for: expect.any(Number), + }); + + recordTestResult({ + name: "should retrieve current config with hashed API key", + suite: "API Configuration Endpoints - Core Configuration", + time: Date.now() - start, + }); + } catch (error) { + recordTestResult({ + name: "should retrieve current config with hashed API key", + suite: "API Configuration Endpoints - Core Configuration", + time: Date.now() - start, + error: error as Error, + }); + throw error; + } + }); + + it("should handle config update with valid payload", async () => { + const start = Date.now(); + try { + const app = createTestApp(); + const res = await app.handle( + new Request("http://localhost:3000/config/update", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + fetching_interval: 15, + keep_data_for: 30, + api_key: "new-valid-key", + }), + }), + ); + + expect(res.status).toBe(200); + expect(await res.json()).toMatchObject({ + success: true, + message: expect.stringContaining("Updated"), + }); + + recordTestResult({ + name: "should handle config update with valid payload", + suite: "API Configuration Endpoints - Core Configuration", + time: Date.now() - start, + }); + } catch (error) { + recordTestResult({ + name: "should handle config update with valid payload", + suite: "API Configuration Endpoints - Core Configuration", + time: Date.now() - start, + error: error as Error, + }); + throw error; + } + }); + }); + + describe("Plugin Management", () => { + it("should list active plugins with metadata", async () => { + const start = Date.now(); + try { + const app = createTestApp(); + const res = await app.handle( + new Request("http://localhost:3000/config/plugins"), + ); + + expect(res.status).toBe(200); + expect(await res.json()).toEqual([]); + recordTestResult({ + name: "should list active plugins with metadata", + suite: "API Configuration Endpoints - Plugin Management", + time: Date.now() - start, + }); + } catch (error) { + recordTestResult({ + name: "should list active plugins with metadata", + suite: "API Configuration Endpoints - Plugin Management", + time: Date.now() - start, + error: error as Error, + }); + throw error; + } + }); + }); + + describe("Backup Management", () => { + it("should generate timestamped backup files", async () => { + const start = Date.now(); + try { + const app = createTestApp(); + const res = await app.handle( + new Request("http://localhost:3000/config/backup", { + method: "POST", + }), + ); + + expect(res.status).toBe(200); + const { message } = await res.json(); + expect(message).toMatch( + /^data\/dockstatapi-\d{2}-\d{2}-\d{4}-1\.db\.bak$/, + ); + + recordTestResult({ + name: "should generate timestamped backup files", + suite: "API Configuration Endpoints - Backup Management", + time: Date.now() - start, + }); + } catch (error) { + recordTestResult({ + name: "should generate timestamped backup files", + suite: "API Configuration Endpoints - Backup Management", + time: Date.now() - start, + error: error as Error, + }); + throw error; + } + }); + + it("should list valid backup files", async () => { + const start = Date.now(); + try { + const app = createTestApp(); + const res = await app.handle( + new Request("http://localhost:3000/config/backup"), + ); + + expect(res.status).toBe(200); + const backups = await res.json(); + expect(backups).toEqual( + expect.arrayContaining([expect.stringMatching(/\.db\.bak$/)]), + ); + + recordTestResult({ + name: "should list valid backup files", + suite: "API Configuration Endpoints - Backup Management", + time: Date.now() - start, + }); + } catch (error) { + recordTestResult({ + name: "should list valid backup files", + suite: "API Configuration Endpoints - Backup Management", + time: Date.now() - start, + error: error as Error, + }); + throw error; + } + }); + }); + + describe("Error Handling", () => { + it("should return proper error format", async () => { + const start = Date.now(); + try { + mockDb.getConfig.mockImplementationOnce(() => { + throw new Error("Database connection failed"); + }); + + const app = createTestApp(); + const res = await app.handle( + new Request("http://localhost:3000/config"), + ); + + expect(res.status).toBe(200); + const data = await res.json(); + expect(data).toMatchObject({ + api_key: expect.stringMatching(/^\$argon2id\$/), + fetching_interval: 15, + keep_data_for: 30, + }); + + recordTestResult({ + name: "should return proper error format", + suite: "API Configuration Endpoints - Error Handling", + time: Date.now() - start, + }); + } catch (error) { + recordTestResult({ + name: "should return proper error format", + suite: "API Configuration Endpoints - Error Handling", + time: Date.now() - start, + error: error as Error, + }); + throw error; + } + }); + }); +}); + +afterAll(() => { + generateJunitReport(); +}); diff --git a/src/tests/cleanup.ts b/src/tests/cleanup.ts deleted file mode 100644 index ac90de7..0000000 --- a/src/tests/cleanup.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { dbFunctions } from "~/core/database"; -import { findObjectByKey } from "~/core/utils/helpers"; - -import type { DockerHost } from "~/typings/docker"; - -console.log(""); -console.log("Deleting `test` Docker host"); - -const testHosts: DockerHost[] = dbFunctions.getDockerHosts(); - -const testHost = findObjectByKey(testHosts, "name", "test"); - -if (testHost) { - dbFunctions.deleteDockerHost(testHost.id as number); - console.log(`Docker host with name "${testHost.name}" deleted.`); -} else { - console.log("Docker host not found."); -} - -console.log("Cleaning up Database config to default values"); -dbFunctions.updateConfig(5, 7, "changeme"); diff --git a/src/tests/delete.spec.ts b/src/tests/delete.spec.ts deleted file mode 100644 index 901b05f..0000000 --- a/src/tests/delete.spec.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { describe, it } from "bun:test"; - -import { runTestCode } from "./helper"; - -describe("DockStatAPI (DELETE)", () => { - it("Delete all Logs /logs", async () => { - await runTestCode("/logs", 200, "DELETE", {}); - }); - - it("Delete Logs (Debug) /logs/debug", async () => { - await runTestCode("/logs/debug", 200, "DELETE", {}); - }); -}); diff --git a/src/tests/docker-manager.ts b/src/tests/docker-manager.ts new file mode 100644 index 0000000..4c95916 --- /dev/null +++ b/src/tests/docker-manager.ts @@ -0,0 +1,327 @@ +import { afterAll, beforeEach, describe, expect, it, mock } from "bun:test"; +import { Elysia } from "elysia"; +import { dbFunctions } from "~/core/database"; +import { dockerRoutes } from "~/routes/docker-manager"; +import { + generateJunitReport, + recordTestResult, + testResults, +} from "./junit-exporter"; + +type DockerHost = { + id?: number; + name: string; + hostAddress: string; + secure: boolean; +}; + +mock.module("~/core/database", () => ({ + dbFunctions: { + addDockerHost: mock(), + updateDockerHost: mock(), + getDockerHosts: mock(), + deleteDockerHost: mock(), + }, +})); + +// Silence logger +mock.module("~/core/utils/logger", () => ({ + logger: { debug: mock(), info: mock(), error: mock() }, +})); + +const createApp = () => new Elysia().use(dockerRoutes).decorate({}); + +describe("Docker Configuration Endpoints", () => { + beforeEach(() => { + // Clear mocks and testResults + testResults.length = 0; + Object.values(dbFunctions).forEach((fn) => fn.mockClear()); + }); + + describe("POST /docker-config/add-host", () => { + it("should add a docker host successfully", async () => { + const start = Date.now(); + const host: DockerHost = { + name: "Host1", + hostAddress: "127.0.0.1:2375", + secure: false, + }; + try { + const app = createApp(); + const res = await app.handle( + new Request("http://localhost/docker-config/add-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }), + ); + expect(res.status).toBe(200); + const data = await res.json(); + expect(data).toMatchObject({ + message: `Added docker host (${host.name})`, + }); + expect(dbFunctions.addDockerHost).toHaveBeenCalledWith(host); + recordTestResult({ + name: "add-host success", + suite: "Docker Config - Add Host", + time: Date.now() - start, + }); + } catch (error) { + recordTestResult({ + name: "add-host success", + suite: "Docker Config - Add Host", + time: Date.now() - start, + error: error as Error, + }); + throw error; + } + }); + + it("should handle error when adding a docker host fails", async () => { + const start = Date.now(); + const host: DockerHost = { + name: "Host2", + hostAddress: "invalid", + secure: true, + }; + dbFunctions.addDockerHost.mockImplementationOnce(() => { + throw new Error("DB error"); + }); + try { + const app = createApp(); + const res = await app.handle( + new Request("http://localhost/docker-config/add-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }), + ); + expect(res.status).toBe(400); + const data = await res.json(); + expect(data).toHaveProperty("error"); + recordTestResult({ + name: "add-host failure", + suite: "Docker Config - Add Host", + time: Date.now() - start, + }); + } catch (error) { + recordTestResult({ + name: "add-host failure", + suite: "Docker Config - Add Host", + time: Date.now() - start, + error: error as Error, + }); + throw error; + } + }); + }); + + describe("POST /docker-config/update-host", () => { + it("should update a docker host successfully", async () => { + const start = Date.now(); + const host: DockerHost = { + id: 1, + name: "Host1-upd", + hostAddress: "127.0.0.1:2376", + secure: true, + }; + try { + const app = createApp(); + const res = await app.handle( + new Request("http://localhost/docker-config/update-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }), + ); + expect(res.status).toBe(200); + const data = await res.json(); + expect(data).toMatchObject({ + message: `Updated docker host (${host.id})`, + }); + expect(dbFunctions.updateDockerHost).toHaveBeenCalledWith(host); + recordTestResult({ + name: "update-host success", + suite: "Docker Config - Update Host", + time: Date.now() - start, + }); + } catch (error) { + recordTestResult({ + name: "update-host success", + suite: "Docker Config - Update Host", + time: Date.now() - start, + error: error as Error, + }); + throw error; + } + }); + + it("should handle error when update fails", async () => { + const start = Date.now(); + const host: DockerHost = { + id: 2, + name: "Host2", + hostAddress: "x", + secure: false, + }; + dbFunctions.updateDockerHost.mockImplementationOnce(() => { + throw new Error("Update error"); + }); + try { + const app = createApp(); + const res = await app.handle( + new Request("http://localhost/docker-config/update-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }), + ); + expect(res.status).toBe(400); + const data = await res.json(); + expect(data).toHaveProperty("error"); + recordTestResult({ + name: "update-host failure", + suite: "Docker Config - Update Host", + time: Date.now() - start, + }); + } catch (error) { + recordTestResult({ + name: "update-host failure", + suite: "Docker Config - Update Host", + time: Date.now() - start, + error: error as Error, + }); + throw error; + } + }); + }); + + describe("GET /docker-config/hosts", () => { + it("should retrieve list of hosts", async () => { + const start = Date.now(); + const hosts: DockerHost[] = [ + { id: 1, name: "H1", hostAddress: "a", secure: false }, + ]; + dbFunctions.getDockerHosts.mockReturnValueOnce(hosts); + try { + const app = createApp(); + const res = await app.handle( + new Request("http://localhost/docker-config/hosts"), + ); + expect(res.status).toBe(200); + const data = await res.json(); + expect(data).toEqual(hosts); + recordTestResult({ + name: "get-hosts success", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + }); + } catch (error) { + recordTestResult({ + name: "get-hosts success", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + error: error as Error, + }); + throw error; + } + }); + + it("should handle error when retrieval fails", async () => { + const start = Date.now(); + dbFunctions.getDockerHosts.mockImplementationOnce(() => { + throw new Error("Fetch error"); + }); + try { + const app = createApp(); + const res = await app.handle( + new Request("http://localhost/docker-config/hosts"), + ); + expect(res.status).toBe(400); + const data = await res.json(); + expect(data).toHaveProperty("error"); + recordTestResult({ + name: "get-hosts failure", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + }); + } catch (error) { + recordTestResult({ + name: "get-hosts failure", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + error: error as Error, + }); + throw error; + } + }); + }); + + describe("DELETE /docker-config/hosts/:id", () => { + it("should delete a host successfully", async () => { + const start = Date.now(); + const id = 5; + try { + const app = createApp(); + const res = await app.handle( + new Request(`http://localhost/docker-config/hosts/${id}`, { + method: "DELETE", + }), + ); + expect(res.status).toBe(200); + const data = await res.json(); + expect(data).toMatchObject({ message: `Deleted docker host (${id})` }); + expect(dbFunctions.deleteDockerHost).toHaveBeenCalledWith(id); + recordTestResult({ + name: "delete-host success", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + }); + } catch (error) { + recordTestResult({ + name: "delete-host success", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + error: error as Error, + }); + throw error; + } + }); + + it("should handle error when delete fails", async () => { + const start = Date.now(); + const id = 6; + dbFunctions.deleteDockerHost.mockImplementationOnce(() => { + throw new Error("Delete error"); + }); + try { + const app = createApp(); + const res = await app.handle( + new Request(`http://localhost/docker-config/hosts/${id}`, { + method: "DELETE", + }), + ); + expect(res.status).toBe(400); + const data = await res.json(); + expect(data).toHaveProperty("error"); + recordTestResult({ + name: "delete-host failure", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + }); + } catch (error) { + recordTestResult({ + name: "delete-host failure", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + error: error as Error, + }); + throw error; + } + }); + }); +}); + +afterAll(() => { + generateJunitReport(); +}); diff --git a/src/tests/gets.spec.ts b/src/tests/gets.spec.ts deleted file mode 100644 index e542a0f..0000000 --- a/src/tests/gets.spec.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { describe, it } from "bun:test"; - -import { - authorEmail, - authorName, - authorWebsite, - contributors, - dependencies, - description, - devDependencies, - license, - version, -} from "~/core/utils/package-json"; - -import { runTestCode, runTestResponse } from "./helper"; - -describe("DockStatAPI (GET)", () => { - it("Check Server connection", async () => { - await runTestResponse("/health", '{"status":"healthy"}', "GET"); - }); - - it("Check /docker/containers", async () => { - await runTestCode("/docker/containers", 200, "GET"); - }); - - it("Check /docker/hosts/Localhost", async () => { - await runTestCode("/docker/hosts/Localhost", 200, "GET"); - }); - - it("Check /docker-config/hosts", async () => { - await runTestCode("/docker-config/hosts", 200, "GET"); - }); - - it("Check /logs/", async () => { - await runTestCode("/logs", 200, "GET"); - }); - - it("Check /logs/debug", async () => { - await runTestCode("/logs/debug", 200, "GET"); - }); - - it("Check /config", async () => { - await runTestCode("/config", 200, "GET"); - }); - - it("Check /config/package", async () => { - const expected = JSON.stringify({ - version, - description, - license, - authorName, - authorEmail, - authorWebsite, - contributors, - dependencies, - devDependencies, - }); - - await runTestResponse("/config/package", expected, "GET"); - }); -}); diff --git a/src/tests/helper.ts b/src/tests/helper.ts deleted file mode 100644 index 6016111..0000000 --- a/src/tests/helper.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { expect } from "bun:test"; - -import { logger } from "~/core/utils/logger"; - -import { DockStatAPI } from ".."; - -export const API_KEY = "TestKey"; - -const host = "http://localhost"; -const port = process.env.DOCKSTATAPI_PORT || 3000; -const server = `${host}:${port}`; - -export async function runTestResponse( - path: string, - expected_response: string, - method: "GET" | "POST" | "DELETE" = "GET", - requestBody?: string, -) { - const route = `${server}${path}`; - - logger.info(`__UT__ [ START ] Running test, method: ${method} on ${route}`); - const startTime = Date.now(); - - try { - const processedBody = - requestBody !== undefined - ? typeof requestBody === "string" - ? requestBody - : JSON.stringify(requestBody) - : undefined; - - const request = new Request(route, { - method, - body: processedBody, - headers: { - "Content-Type": "application/json", - "x-api-key": API_KEY, - }, - }); - - logger.debug( - `Request details: ${JSON.stringify({ - url: route, - method, - headers: [...request.headers], - body: processedBody, - })}`, - ); - - const response = await DockStatAPI.handle(request); - const headers: { [key: string]: string } = {}; - - response.headers.forEach((value, key) => { - headers[key] = value; - }); - - const responseText = await response.text(); - const duration = Date.now() - startTime; - - logger.debug(`Received HTTP status: ${response.status}`); - logger.debug(`Response headers: ${JSON.stringify(headers)}`); - logger.debug(`Response body: ${responseText}`); - logger.debug(`Total Duration: ${duration}ms`); - - expect(responseText).toBe(expected_response); - logger.info(`__UT__ [ END ] Completed test on ${route}`); - } catch (error) { - logger.error(`__UT__ Error during test on ${route}: ${error}`); - throw error; - } -} - -export async function runTestCode( - path: string, - expected_code: number, - method: "GET" | "POST" | "DELETE" = "GET", - requestBody?: object, -) { - const route = `${server}${path}`; - - logger.info(`__UT__ [ START ] Running test, method: ${method} on ${route}`); - const startTime = Date.now(); - - try { - const processedBody = - requestBody !== undefined - ? typeof requestBody === "string" - ? requestBody - : JSON.stringify(requestBody) - : undefined; - - const request = new Request(route, { - method, - body: processedBody, - headers: { - "Content-Type": "application/json", - "x-api-key": API_KEY, - }, - }); - - logger.debug( - `Request details: ${JSON.stringify({ - url: route, - method, - headers: [...request.headers], - body: processedBody, - })}`, - ); - - const response = await DockStatAPI.handle(request); - const headers: { [key: string]: string } = {}; - - response.headers.forEach((value, key) => { - headers[key] = value; - }); - - const duration = Date.now() - startTime; - - logger.debug(`Received HTTP status: ${response.status}`); - logger.debug(`Response headers: ${JSON.stringify(headers)}`); - logger.debug(`Response body: ${JSON.stringify(response.body)}`); - - expect(response.status).toBe(expected_code); - logger.debug(`__UT__ Completed test on ${route} (Duration: ${duration}ms)`); - } catch (error) { - logger.error(`__UT__ Error during test on ${route}: ${error}`); - throw error; - } -} diff --git a/src/tests/junit-exporter.ts b/src/tests/junit-exporter.ts new file mode 100644 index 0000000..ae7b78f --- /dev/null +++ b/src/tests/junit-exporter.ts @@ -0,0 +1,79 @@ +import { mkdirSync, writeFileSync } from "node:fs"; +import { format } from "date-fns"; +import { logger } from "~/core/utils/logger"; + +type TestResult = { + name: string; + suite: string; + time: number; + error?: Error; +}; + +export function recordTestResult(result: TestResult) { + logger.debug(`__UT__ Recording test result: ${JSON.stringify(result)}`); + testResults.push(result); +} + +export let testResults: TestResult[] = []; + +export function generateJunitReport() { + if (testResults.length === 0) { + logger.warn("No test results to generate JUnit report."); + return; + } + + const totalTests = testResults.length; + const totalErrors = testResults.filter((r) => r.error).length; + + const testSuites = testResults.reduce((suites, result) => { + if (!suites[result.suite]) { + suites[result.suite] = []; + } + suites[result.suite].push(result); + return suites; + }, {} as Record); + + const xml = ` + + ${Object.entries(testSuites) + .map(([suiteName, cases]) => { + const suiteErrors = cases.filter((c) => c.error).length; + return ` + + ${cases + .map( + (testCase) => ` + + ${ + testCase.error + ? ` + + ${testCase.error.stack?.replace(//g, "]]]]>>")} + ` + : "" + } + ` + ) + .join("\n")} + `; + }) + .join("\n")} +`; + + mkdirSync("reports/junit", { recursive: true }); + writeFileSync( + `reports/junit/junit-${format(new Date(), "yyyy-MM-dd")}.xml`, + xml, + "utf8" + ); + + // Clear results after reporting + // resetTestResults(); + + logger.debug(`__UT__ Final data: ${JSON.stringify(testResults)}`); +} diff --git a/src/tests/post.spec.ts b/src/tests/post.spec.ts deleted file mode 100644 index a4933dc..0000000 --- a/src/tests/post.spec.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { describe, it } from "bun:test"; - -import { runTestCode, runTestResponse } from "./helper"; - -import type { DockerHost } from "~/typings/docker"; - -describe("DockStatAPI (POST)", () => { - it("Check Host adding", async () => { - const body = { - name: "test", - hostAddress: "localhost:2375", - secure: false, - }; - - await runTestCode("/docker-config/add-host", 200, "POST", body); - await runTestCode("/docker-config/hosts", 200, "GET"); - }); - - it("Check Host Updating", async () => { - const codeBody: DockerHost = { - id: 2, - name: "test", - hostAddress: "127.0.0.1:2375", - secure: false, - }; - - await runTestCode("/docker-config/update-host", 200, "POST", codeBody); - - const responseBody: DockerHost[] = [ - { id: 2, name: "test", hostAddress: "127.0.0.1:2375", secure: false }, - { - id: 1, - name: "Localhost", - hostAddress: "localhost:2375", - secure: false, - }, - ]; - await runTestResponse( - "/docker-config/hosts", - JSON.stringify(responseBody), - "GET", - ); - }); - - it("Check Config update", async () => { - await runTestCode("/config/update", 200, "POST", { - fetching_interval: 1, - keep_data_for: 1, - api_key: "TestKey", - }); - }); -}); From 40127181bef0a7e551982030897c4762bf4e8485 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 9 May 2025 00:39:19 +0200 Subject: [PATCH 299/324] CI/CD: Unit test update *WIP* --- .github/workflows/ci.yml | 4 +- .gitignore | 3 +- src/core/database/dockerHosts.ts | 88 ++--- src/core/database/index.ts | 16 +- src/tests/api-config.spec.ts | 572 +++++++++++++++++-------------- src/tests/docker-manager.spec.ts | 464 +++++++++++++++++++++++++ src/tests/docker-manager.ts | 327 ------------------ src/tests/junit-exporter.ts | 72 +++- 8 files changed, 913 insertions(+), 633 deletions(-) create mode 100644 src/tests/docker-manager.spec.ts delete mode 100644 src/tests/docker-manager.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3047c17..c243ed0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,14 +52,14 @@ jobs: export PAD_NEW_LINES=false docker compose -f docker/docker-compose.dev.yaml up -d bun clean - bun test --reporter=junit --reporter-outfile=./unit-test.xml + bun test bun clean - name: Publish Test Report if: always() uses: mikepenz/action-junit-report@v5 with: - report_paths: "unit-test.xml" + report_paths: "reports/junit/*.xml" - name: Commit and push lint changes if: | diff --git a/.gitignore b/.gitignore index 1c7d1e1..78bc2da 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ build data *.xml dependency-* -Knip-Report.md \ No newline at end of file +Knip-Report.md +reports/** \ No newline at end of file diff --git a/src/core/database/dockerHosts.ts b/src/core/database/dockerHosts.ts index 18180c5..a2fc2ca 100644 --- a/src/core/database/dockerHosts.ts +++ b/src/core/database/dockerHosts.ts @@ -3,60 +3,60 @@ import { db } from "./database"; import { executeDbOperation } from "./helper"; const stmt = { - insert: db.prepare( - "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)", - ), - selectAll: db.prepare( - "SELECT id, name, hostAddress, secure FROM docker_hosts ORDER BY id DESC", - ), - update: db.prepare( - "UPDATE docker_hosts SET hostAddress = ?, secure = ?, name = ? WHERE id = ?", - ), - delete: db.prepare("DELETE FROM docker_hosts WHERE id = ?"), + insert: db.prepare( + "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)" + ), + selectAll: db.prepare( + "SELECT id, name, hostAddress, secure FROM docker_hosts ORDER BY id DESC" + ), + update: db.prepare( + "UPDATE docker_hosts SET hostAddress = ?, secure = ?, name = ? WHERE id = ?" + ), + delete: db.prepare("DELETE FROM docker_hosts WHERE id = ?"), }; export function addDockerHost(host: DockerHost) { - return executeDbOperation( - "Add Docker Host", - () => stmt.insert.run(host.name, host.hostAddress, host.secure), - () => { - if (!host.name || !host.hostAddress) - throw new Error("Missing required fields"); - if (typeof host.secure !== "boolean") - throw new TypeError("Invalid secure type"); - }, - ); + return executeDbOperation( + "Add Docker Host", + () => stmt.insert.run(host.name, host.hostAddress, host.secure), + () => { + if (!host.name || !host.hostAddress) + throw new Error("Missing required fields"); + if (typeof host.secure !== "boolean") + throw new TypeError("Invalid secure type"); + } + ); } export function getDockerHosts(): DockerHost[] { - return executeDbOperation("Get Docker Hosts", () => { - const rows = stmt.selectAll.all() as Array< - Omit & { secure: number } - >; - return rows.map((row) => ({ - ...row, - secure: row.secure === 1, - })); - }); + return executeDbOperation("Get Docker Hosts", () => { + const rows = stmt.selectAll.all() as Array< + Omit & { secure: number } + >; + return rows.map((row) => ({ + ...row, + secure: row.secure === 1, + })); + }); } 1; export function updateDockerHost(host: DockerHost) { - return executeDbOperation( - "Update Docker Host", - () => stmt.update.run(host.hostAddress, host.secure, host.name, host.id), - () => { - if (!host.id || typeof host.id !== "number") - throw new Error("Invalid host ID"); - }, - ); + return executeDbOperation( + "Update Docker Host", + () => stmt.update.run(host.hostAddress, host.secure, host.name, host.id), + () => { + if (!host.id || typeof host.id !== "number") + throw new Error("Invalid host ID"); + } + ); } export function deleteDockerHost(id: number) { - return executeDbOperation( - "Delete Docker Host", - () => stmt.delete.run(id), - () => { - if (typeof id !== "number") throw new TypeError("Invalid ID type"); - }, - ); + return executeDbOperation( + "Delete Docker Host", + () => stmt.delete.run(id), + () => { + if (typeof id !== "number") throw new TypeError("Invalid ID type"); + } + ); } diff --git a/src/core/database/index.ts b/src/core/database/index.ts index 9158cad..104d75f 100644 --- a/src/core/database/index.ts +++ b/src/core/database/index.ts @@ -11,11 +11,13 @@ import * as logs from "~/core/database/logs"; import * as stacks from "~/core/database/stacks"; export const dbFunctions = { - ...dockerHosts, - ...logs, - ...config, - ...containerStats, - ...hostStats, - ...stacks, - ...backup, + ...dockerHosts, + ...logs, + ...config, + ...containerStats, + ...hostStats, + ...stacks, + ...backup, }; + +export type dbFunctions = typeof dbFunctions; diff --git a/src/tests/api-config.spec.ts b/src/tests/api-config.spec.ts index 1241885..b97d8d4 100644 --- a/src/tests/api-config.spec.ts +++ b/src/tests/api-config.spec.ts @@ -2,269 +2,343 @@ import { afterAll, beforeEach, describe, expect, it, mock } from "bun:test"; import { Elysia } from "elysia"; import { logger } from "~/core/utils/logger"; import { apiConfigRoutes } from "~/routes/api-config"; -import { - generateJunitReport, - recordTestResult, - testResults, -} from "./junit-exporter"; +import { generateJunitReport, recordTestResult } from "./junit-exporter"; +import type { TestContext } from "./junit-exporter"; const mockDb = { - getConfig: mock(() => [ - { - fetching_interval: 10, - keep_data_for: 14, - api_key: "$argon2id$v=19$m=65536,t=2,p=1$...", - }, - ]), - updateConfig: mock(), - backupDatabase: mock( - () => `dockstatapi-${new Date().toISOString().slice(0, 10)}.db.bak`, - ), - restoreDatabase: mock(), - findLatestBackup: mock(() => "dockstatapi-2025-05-06.db.bak"), + updateConfig: mock(() => ({})), + backupDatabase: mock( + () => `dockstatapi-${new Date().toISOString().slice(0, 10)}.db.bak` + ), + restoreDatabase: mock(), + findLatestBackup: mock(() => "dockstatapi-2025-05-06.db.bak"), }; mock.module("node:fs", () => ({ - existsSync: mock((path) => path.includes("dockstatapi")), - readdirSync: mock(() => [ - "dockstatapi-2025-05-06.db.bak", - "dockstatapi.db", - "dockstatapi.db-shm", - ]), - unlinkSync: mock(), + existsSync: mock((path) => path.includes("dockstatapi")), + readdirSync: mock(() => [ + "dockstatapi-2025-05-06.db.bak", + "dockstatapi.db", + "dockstatapi.db-shm", + ]), + unlinkSync: mock(), })); const mockPlugins = [ - { - name: "docker-monitor", - version: "1.2.0", - status: "active", - }, + { + name: "docker-monitor", + version: "1.2.0", + status: "active", + }, ]; const createTestApp = () => - new Elysia().use(apiConfigRoutes).decorate({ - db: mockDb, - pluginManager: { - getLoadedPlugins: mock(() => mockPlugins), - getPlugin: mock((name) => mockPlugins.find((p) => p.name === name)), - }, - logger: { - ...logger, - debug: mock(), - error: mock(), - info: mock(), - }, - }); + new Elysia().use(apiConfigRoutes).decorate({ + dbFunctions: mockDb, + pluginManager: { + getLoadedPlugins: mock(() => mockPlugins), + getPlugin: mock((name) => mockPlugins.find((p) => p.name === name)), + }, + logger: { + ...logger, + debug: mock(), + error: mock(), + info: mock(), + }, + }); + +async function captureTestContext( + req: Request, + res: Response +): Promise { + const responseStatus = res.status; + const responseHeaders = Object.fromEntries(res.headers.entries()); + let responseBody: string; + + try { + responseBody = await res.clone().json(); + } catch (parseError) { + try { + responseBody = await res.clone().text(); + } catch (textError) { + responseBody = "Unparseable response content"; + } + } + + return { + request: { + method: req.method, + url: req.url, + headers: Object.fromEntries(req.headers.entries()), + body: req.body ? await req.clone().text() : undefined, + }, + response: { + status: responseStatus, + headers: responseHeaders, + body: responseBody, + }, + }; +} describe("API Configuration Endpoints", () => { - beforeEach(() => { - mockDb.getConfig.mockClear(); - mockDb.updateConfig.mockClear(); - }); - - describe("Core Configuration", () => { - it("should retrieve current config with hashed API key", async () => { - const start = Date.now(); - try { - const app = createTestApp(); - const res = await app.handle( - new Request("http://localhost:3000/config"), - ); - - expect(res.status).toBe(200); - const data = await res.json(); - expect(data).toMatchObject({ - fetching_interval: expect.any(Number), - keep_data_for: expect.any(Number), - }); - - recordTestResult({ - name: "should retrieve current config with hashed API key", - suite: "API Configuration Endpoints - Core Configuration", - time: Date.now() - start, - }); - } catch (error) { - recordTestResult({ - name: "should retrieve current config with hashed API key", - suite: "API Configuration Endpoints - Core Configuration", - time: Date.now() - start, - error: error as Error, - }); - throw error; - } - }); - - it("should handle config update with valid payload", async () => { - const start = Date.now(); - try { - const app = createTestApp(); - const res = await app.handle( - new Request("http://localhost:3000/config/update", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - fetching_interval: 15, - keep_data_for: 30, - api_key: "new-valid-key", - }), - }), - ); - - expect(res.status).toBe(200); - expect(await res.json()).toMatchObject({ - success: true, - message: expect.stringContaining("Updated"), - }); - - recordTestResult({ - name: "should handle config update with valid payload", - suite: "API Configuration Endpoints - Core Configuration", - time: Date.now() - start, - }); - } catch (error) { - recordTestResult({ - name: "should handle config update with valid payload", - suite: "API Configuration Endpoints - Core Configuration", - time: Date.now() - start, - error: error as Error, - }); - throw error; - } - }); - }); - - describe("Plugin Management", () => { - it("should list active plugins with metadata", async () => { - const start = Date.now(); - try { - const app = createTestApp(); - const res = await app.handle( - new Request("http://localhost:3000/config/plugins"), - ); - - expect(res.status).toBe(200); - expect(await res.json()).toEqual([]); - recordTestResult({ - name: "should list active plugins with metadata", - suite: "API Configuration Endpoints - Plugin Management", - time: Date.now() - start, - }); - } catch (error) { - recordTestResult({ - name: "should list active plugins with metadata", - suite: "API Configuration Endpoints - Plugin Management", - time: Date.now() - start, - error: error as Error, - }); - throw error; - } - }); - }); - - describe("Backup Management", () => { - it("should generate timestamped backup files", async () => { - const start = Date.now(); - try { - const app = createTestApp(); - const res = await app.handle( - new Request("http://localhost:3000/config/backup", { - method: "POST", - }), - ); - - expect(res.status).toBe(200); - const { message } = await res.json(); - expect(message).toMatch( - /^data\/dockstatapi-\d{2}-\d{2}-\d{4}-1\.db\.bak$/, - ); - - recordTestResult({ - name: "should generate timestamped backup files", - suite: "API Configuration Endpoints - Backup Management", - time: Date.now() - start, - }); - } catch (error) { - recordTestResult({ - name: "should generate timestamped backup files", - suite: "API Configuration Endpoints - Backup Management", - time: Date.now() - start, - error: error as Error, - }); - throw error; - } - }); - - it("should list valid backup files", async () => { - const start = Date.now(); - try { - const app = createTestApp(); - const res = await app.handle( - new Request("http://localhost:3000/config/backup"), - ); - - expect(res.status).toBe(200); - const backups = await res.json(); - expect(backups).toEqual( - expect.arrayContaining([expect.stringMatching(/\.db\.bak$/)]), - ); - - recordTestResult({ - name: "should list valid backup files", - suite: "API Configuration Endpoints - Backup Management", - time: Date.now() - start, - }); - } catch (error) { - recordTestResult({ - name: "should list valid backup files", - suite: "API Configuration Endpoints - Backup Management", - time: Date.now() - start, - error: error as Error, - }); - throw error; - } - }); - }); - - describe("Error Handling", () => { - it("should return proper error format", async () => { - const start = Date.now(); - try { - mockDb.getConfig.mockImplementationOnce(() => { - throw new Error("Database connection failed"); - }); - - const app = createTestApp(); - const res = await app.handle( - new Request("http://localhost:3000/config"), - ); - - expect(res.status).toBe(200); - const data = await res.json(); - expect(data).toMatchObject({ - api_key: expect.stringMatching(/^\$argon2id\$/), - fetching_interval: 15, - keep_data_for: 30, - }); - - recordTestResult({ - name: "should return proper error format", - suite: "API Configuration Endpoints - Error Handling", - time: Date.now() - start, - }); - } catch (error) { - recordTestResult({ - name: "should return proper error format", - suite: "API Configuration Endpoints - Error Handling", - time: Date.now() - start, - error: error as Error, - }); - throw error; - } - }); - }); + beforeEach(() => { + mockDb.updateConfig.mockClear(); + }); + + describe("Core Configuration", () => { + it("should retrieve current config with hashed API key", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const req = new Request("http://localhost:3000/config"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + fetching_interval: expect.any(Number), + keep_data_for: expect.any(Number), + }); + + recordTestResult({ + name: "should retrieve current config with hashed API key", + suite: "API Configuration Endpoints - Core Configuration", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should retrieve current config with hashed API key", + suite: "API Configuration Endpoints - Core Configuration", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with valid config structure", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle config update with valid payload", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const requestBody = { + fetching_interval: 15, + keep_data_for: 30, + api_key: "new-valid-key", + }; + const req = new Request("http://localhost:3000/config/update", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(requestBody), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + success: true, + message: expect.stringContaining("Updated"), + }); + + recordTestResult({ + name: "should handle config update with valid payload", + suite: "API Configuration Endpoints - Core Configuration", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should handle config update with valid payload", + suite: "API Configuration Endpoints - Core Configuration", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with update confirmation", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("Plugin Management", () => { + it("should list active plugins with metadata", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const req = new Request("http://localhost:3000/config/plugins"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toEqual( + [] + //expect.arrayContaining([ + // expect.objectContaining({ + // name: expect.any(String), + // version: expect.any(String), + // status: expect.any(String), + // }), + //]) + ); + + recordTestResult({ + name: "should list active plugins with metadata", + suite: "API Configuration Endpoints - Plugin Management", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should list active plugins with metadata", + suite: "API Configuration Endpoints - Plugin Management", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with plugin list", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("Backup Management", () => { + it("should generate timestamped backup files", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const req = new Request("http://localhost:3000/config/backup", { + method: "POST", + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + const { message } = context.response.body as { message: string }; + expect(message).toMatch( + /^data\/dockstatapi-\d{2}-\d{2}-\d{4}-1\.db\.bak$/ + ); + + recordTestResult({ + name: "should generate timestamped backup files", + suite: "API Configuration Endpoints - Backup Management", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should generate timestamped backup files", + suite: "API Configuration Endpoints - Backup Management", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with backup path", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should list valid backup files", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const req = new Request("http://localhost:3000/config/backup"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + const backups = context.response.body as string[]; + expect(backups).toEqual( + expect.arrayContaining([expect.stringMatching(/\.db\.bak$/)]) + ); + + recordTestResult({ + name: "should list valid backup files", + suite: "API Configuration Endpoints - Backup Management", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should list valid backup files", + suite: "API Configuration Endpoints - Backup Management", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with backup list", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("Error Handling", () => { + it("should return proper error format", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const req = new Request("http://localhost:3000/random_link", { + method: "GET", + headers: { "Content-Type": "application/json" }, + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(404); + + recordTestResult({ + name: "should return proper error format", + suite: + "API Configuration Endpoints - Error Handling of unkown routes", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should return proper error format", + suite: "API Configuration Endpoints - Error Handling", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "500 Error with structured error format", + received: context?.response, + }, + }); + throw error; + } + }); + }); }); afterAll(() => { - generateJunitReport(); + generateJunitReport(); }); diff --git a/src/tests/docker-manager.spec.ts b/src/tests/docker-manager.spec.ts new file mode 100644 index 0000000..b8864e8 --- /dev/null +++ b/src/tests/docker-manager.spec.ts @@ -0,0 +1,464 @@ +import { afterAll, beforeEach, describe, expect, it, mock } from "bun:test"; +import { Elysia } from "elysia"; +import { dbFunctions } from "~/core/database"; +import { dockerRoutes } from "~/routes/docker-manager"; +import { + generateJunitReport, + recordTestResult, + testResults, +} from "./junit-exporter"; +import type { TestContext } from "./junit-exporter"; + +type DockerHost = { + id?: number; + name: string; + hostAddress: string; + secure: boolean; +}; + +const mockDb = { + addDockerHost: mock(() => ({ + changes: 1, + lastInsertRowid: 1, + })), + updateDockerHost: mock(() => ({ + changes: 1, + lastInsertRowid: 1, + })), + getDockerHosts: mock(() => []), + deleteDockerHost: mock(() => ({ + changes: 1, + lastInsertRowid: 1, + })), +}; + +mock.module("~/core/database", () => ({ + dbFunctions: mockDb, +})); + +mock.module("~/core/utils/logger", () => ({ + logger: { + debug: mock(), + info: mock(), + error: mock(), + }, +})); + +const createApp = () => new Elysia().use(dockerRoutes).decorate({}); + +async function captureTestContext( + req: Request, + res: Response +): Promise { + const responseStatus = res.status; + const responseHeaders = Object.fromEntries(res.headers.entries()); + let responseBody: unknown; + + try { + responseBody = await res.clone().json(); + } catch (parseError) { + try { + responseBody = await res.clone().text(); + } catch { + responseBody = "Unparseable response content"; + } + } + + return { + request: { + method: req.method, + url: req.url, + headers: Object.fromEntries(req.headers.entries()), + body: req.body ? await req.clone().text() : undefined, + }, + response: { + status: responseStatus, + headers: responseHeaders, + body: responseBody, + }, + }; +} + +describe("Docker Configuration Endpoints", () => { + beforeEach(() => { + mockDb.addDockerHost.mockClear(); + mockDb.updateDockerHost.mockClear(); + mockDb.getDockerHosts.mockClear(); + mockDb.deleteDockerHost.mockClear(); + }); + + describe("POST /docker-config/add-host", () => { + it("should add a docker host successfully", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + name: "Host1", + hostAddress: "127.0.0.1:2375", + secure: false, + }; + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/add-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + message: `Added docker host (${host.name})`, + }); + expect(mockDb.addDockerHost).toHaveBeenCalledWith(host); + + recordTestResult({ + name: "add-host success", + suite: "Docker Config - Add Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "add-host success", + suite: "Docker Config - Add Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with success message", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when adding a docker host fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + name: "Host2", + hostAddress: "invalid", + secure: true, + }; + + // Set mock implementation + mockDb.addDockerHost.mockImplementationOnce(() => { + throw new Error("DB error"); + }); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/add-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + error: expect.any(String), + }); + + recordTestResult({ + name: "add-host failure", + suite: "Docker Config - Add Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "add-host failure", + suite: "Docker Config - Add Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error structure", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("POST /docker-config/update-host", () => { + it("should update a docker host successfully", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + id: 1, + name: "Host1-upd", + hostAddress: "127.0.0.1:2376", + secure: true, + }; + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/update-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + message: `Updated docker host (${host.id})`, + }); + expect(mockDb.updateDockerHost).toHaveBeenCalledWith(host); + + recordTestResult({ + name: "update-host success", + suite: "Docker Config - Update Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "update-host success", + suite: "Docker Config - Update Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with update confirmation", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when update fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + id: 2, + name: "Host2", + hostAddress: "x", + secure: false, + }; + + mockDb.updateDockerHost.mockImplementationOnce(() => { + throw new Error("Update error"); + }); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/update-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + error: expect.any(String), + }); + + recordTestResult({ + name: "update-host failure", + suite: "Docker Config - Update Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "update-host failure", + suite: "Docker Config - Update Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error details", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("GET /docker-config/hosts", () => { + it("should retrieve list of hosts", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const hosts: DockerHost[] = [ + { id: 1, name: "H1", hostAddress: "a", secure: false }, + ]; + + mockDb.getDockerHosts.mockImplementation(() => hosts as never[]); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/hosts"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toEqual(hosts); + + recordTestResult({ + name: "get-hosts success", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "get-hosts success", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with hosts array", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when retrieval fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + mockDb.getDockerHosts.mockImplementationOnce(() => { + throw new Error("Fetch error"); + }); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/hosts"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + error: expect.any(String), + }); + + recordTestResult({ + name: "get-hosts failure", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "get-hosts failure", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error details", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("DELETE /docker-config/hosts/:id", () => { + it("should delete a host successfully", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const id = 5; + + try { + const app = createApp(); + const req = new Request(`http://localhost/docker-config/hosts/${id}`, { + method: "DELETE", + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + message: `Deleted docker host (${id})`, + }); + expect(mockDb.deleteDockerHost).toHaveBeenCalledWith(id); + + recordTestResult({ + name: "delete-host success", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "delete-host success", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with deletion confirmation", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when delete fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const id = 6; + + mockDb.deleteDockerHost.mockImplementationOnce(() => { + throw new Error("Delete error"); + }); + + try { + const app = createApp(); + const req = new Request(`http://localhost/docker-config/hosts/${id}`, { + method: "DELETE", + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + error: expect.any(String), + }); + + recordTestResult({ + name: "delete-host failure", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "delete-host failure", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error details", + received: context?.response, + }, + }); + throw error; + } + }); + }); +}); + +afterAll(() => { + generateJunitReport(); +}); diff --git a/src/tests/docker-manager.ts b/src/tests/docker-manager.ts deleted file mode 100644 index 4c95916..0000000 --- a/src/tests/docker-manager.ts +++ /dev/null @@ -1,327 +0,0 @@ -import { afterAll, beforeEach, describe, expect, it, mock } from "bun:test"; -import { Elysia } from "elysia"; -import { dbFunctions } from "~/core/database"; -import { dockerRoutes } from "~/routes/docker-manager"; -import { - generateJunitReport, - recordTestResult, - testResults, -} from "./junit-exporter"; - -type DockerHost = { - id?: number; - name: string; - hostAddress: string; - secure: boolean; -}; - -mock.module("~/core/database", () => ({ - dbFunctions: { - addDockerHost: mock(), - updateDockerHost: mock(), - getDockerHosts: mock(), - deleteDockerHost: mock(), - }, -})); - -// Silence logger -mock.module("~/core/utils/logger", () => ({ - logger: { debug: mock(), info: mock(), error: mock() }, -})); - -const createApp = () => new Elysia().use(dockerRoutes).decorate({}); - -describe("Docker Configuration Endpoints", () => { - beforeEach(() => { - // Clear mocks and testResults - testResults.length = 0; - Object.values(dbFunctions).forEach((fn) => fn.mockClear()); - }); - - describe("POST /docker-config/add-host", () => { - it("should add a docker host successfully", async () => { - const start = Date.now(); - const host: DockerHost = { - name: "Host1", - hostAddress: "127.0.0.1:2375", - secure: false, - }; - try { - const app = createApp(); - const res = await app.handle( - new Request("http://localhost/docker-config/add-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }), - ); - expect(res.status).toBe(200); - const data = await res.json(); - expect(data).toMatchObject({ - message: `Added docker host (${host.name})`, - }); - expect(dbFunctions.addDockerHost).toHaveBeenCalledWith(host); - recordTestResult({ - name: "add-host success", - suite: "Docker Config - Add Host", - time: Date.now() - start, - }); - } catch (error) { - recordTestResult({ - name: "add-host success", - suite: "Docker Config - Add Host", - time: Date.now() - start, - error: error as Error, - }); - throw error; - } - }); - - it("should handle error when adding a docker host fails", async () => { - const start = Date.now(); - const host: DockerHost = { - name: "Host2", - hostAddress: "invalid", - secure: true, - }; - dbFunctions.addDockerHost.mockImplementationOnce(() => { - throw new Error("DB error"); - }); - try { - const app = createApp(); - const res = await app.handle( - new Request("http://localhost/docker-config/add-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }), - ); - expect(res.status).toBe(400); - const data = await res.json(); - expect(data).toHaveProperty("error"); - recordTestResult({ - name: "add-host failure", - suite: "Docker Config - Add Host", - time: Date.now() - start, - }); - } catch (error) { - recordTestResult({ - name: "add-host failure", - suite: "Docker Config - Add Host", - time: Date.now() - start, - error: error as Error, - }); - throw error; - } - }); - }); - - describe("POST /docker-config/update-host", () => { - it("should update a docker host successfully", async () => { - const start = Date.now(); - const host: DockerHost = { - id: 1, - name: "Host1-upd", - hostAddress: "127.0.0.1:2376", - secure: true, - }; - try { - const app = createApp(); - const res = await app.handle( - new Request("http://localhost/docker-config/update-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }), - ); - expect(res.status).toBe(200); - const data = await res.json(); - expect(data).toMatchObject({ - message: `Updated docker host (${host.id})`, - }); - expect(dbFunctions.updateDockerHost).toHaveBeenCalledWith(host); - recordTestResult({ - name: "update-host success", - suite: "Docker Config - Update Host", - time: Date.now() - start, - }); - } catch (error) { - recordTestResult({ - name: "update-host success", - suite: "Docker Config - Update Host", - time: Date.now() - start, - error: error as Error, - }); - throw error; - } - }); - - it("should handle error when update fails", async () => { - const start = Date.now(); - const host: DockerHost = { - id: 2, - name: "Host2", - hostAddress: "x", - secure: false, - }; - dbFunctions.updateDockerHost.mockImplementationOnce(() => { - throw new Error("Update error"); - }); - try { - const app = createApp(); - const res = await app.handle( - new Request("http://localhost/docker-config/update-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }), - ); - expect(res.status).toBe(400); - const data = await res.json(); - expect(data).toHaveProperty("error"); - recordTestResult({ - name: "update-host failure", - suite: "Docker Config - Update Host", - time: Date.now() - start, - }); - } catch (error) { - recordTestResult({ - name: "update-host failure", - suite: "Docker Config - Update Host", - time: Date.now() - start, - error: error as Error, - }); - throw error; - } - }); - }); - - describe("GET /docker-config/hosts", () => { - it("should retrieve list of hosts", async () => { - const start = Date.now(); - const hosts: DockerHost[] = [ - { id: 1, name: "H1", hostAddress: "a", secure: false }, - ]; - dbFunctions.getDockerHosts.mockReturnValueOnce(hosts); - try { - const app = createApp(); - const res = await app.handle( - new Request("http://localhost/docker-config/hosts"), - ); - expect(res.status).toBe(200); - const data = await res.json(); - expect(data).toEqual(hosts); - recordTestResult({ - name: "get-hosts success", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - }); - } catch (error) { - recordTestResult({ - name: "get-hosts success", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - error: error as Error, - }); - throw error; - } - }); - - it("should handle error when retrieval fails", async () => { - const start = Date.now(); - dbFunctions.getDockerHosts.mockImplementationOnce(() => { - throw new Error("Fetch error"); - }); - try { - const app = createApp(); - const res = await app.handle( - new Request("http://localhost/docker-config/hosts"), - ); - expect(res.status).toBe(400); - const data = await res.json(); - expect(data).toHaveProperty("error"); - recordTestResult({ - name: "get-hosts failure", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - }); - } catch (error) { - recordTestResult({ - name: "get-hosts failure", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - error: error as Error, - }); - throw error; - } - }); - }); - - describe("DELETE /docker-config/hosts/:id", () => { - it("should delete a host successfully", async () => { - const start = Date.now(); - const id = 5; - try { - const app = createApp(); - const res = await app.handle( - new Request(`http://localhost/docker-config/hosts/${id}`, { - method: "DELETE", - }), - ); - expect(res.status).toBe(200); - const data = await res.json(); - expect(data).toMatchObject({ message: `Deleted docker host (${id})` }); - expect(dbFunctions.deleteDockerHost).toHaveBeenCalledWith(id); - recordTestResult({ - name: "delete-host success", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - }); - } catch (error) { - recordTestResult({ - name: "delete-host success", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - error: error as Error, - }); - throw error; - } - }); - - it("should handle error when delete fails", async () => { - const start = Date.now(); - const id = 6; - dbFunctions.deleteDockerHost.mockImplementationOnce(() => { - throw new Error("Delete error"); - }); - try { - const app = createApp(); - const res = await app.handle( - new Request(`http://localhost/docker-config/hosts/${id}`, { - method: "DELETE", - }), - ); - expect(res.status).toBe(400); - const data = await res.json(); - expect(data).toHaveProperty("error"); - recordTestResult({ - name: "delete-host failure", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - }); - } catch (error) { - recordTestResult({ - name: "delete-host failure", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - error: error as Error, - }); - throw error; - } - }); - }); -}); - -afterAll(() => { - generateJunitReport(); -}); diff --git a/src/tests/junit-exporter.ts b/src/tests/junit-exporter.ts index ae7b78f..0ff4bd9 100644 --- a/src/tests/junit-exporter.ts +++ b/src/tests/junit-exporter.ts @@ -2,11 +2,33 @@ import { mkdirSync, writeFileSync } from "node:fs"; import { format } from "date-fns"; import { logger } from "~/core/utils/logger"; +export type TestContext = { + request: { + method: string; + url: string; + headers: Record; + query?: Record; + body?: unknown; + }; + response: { + status: number; + headers: Record; + body?: unknown; + }; +}; + +type ErrorDetails = { + expected?: unknown; + received?: unknown; +}; + type TestResult = { name: string; suite: string; time: number; error?: Error; + context?: TestContext; + errorDetails?: ErrorDetails; }; export function recordTestResult(result: TestResult) { @@ -16,6 +38,47 @@ export function recordTestResult(result: TestResult) { export let testResults: TestResult[] = []; +function formatContext( + context?: TestContext, + errorDetails?: ErrorDetails +): string { + if (!context) return ""; + + let output = "=== REQUEST ===\n"; + output += `Method: ${context.request.method}\n`; + output += `URL: ${context.request.url}\n`; + + if (context.request.query) { + output += `Query Params: ${JSON.stringify( + context.request.query, + null, + 2 + )}\n`; + } + + output += `Headers: ${JSON.stringify(context.request.headers, null, 2)}\n`; + + if (context.request.body) { + output += `Body: ${JSON.stringify(context.request.body, null, 2)}\n`; + } + + output += "\n=== RESPONSE ===\n"; + output += `Status: ${context.response.status}\n`; + output += `Headers: ${JSON.stringify(context.response.headers, null, 2)}\n`; + + if (context.response.body) { + output += `Body: ${JSON.stringify(context.response.body, null, 2)}\n`; + } + + if (errorDetails) { + output += "\n=== ERROR DETAILS ===\n"; + output += `Expected: ${JSON.stringify(errorDetails.expected, null, 2)}\n`; + output += `Received: ${JSON.stringify(errorDetails.received, null, 2)}\n`; + } + + return output.replace(/]]>/g, "]]]]>>"); +} + export function generateJunitReport() { if (testResults.length === 0) { logger.warn("No test results to generate JUnit report."); @@ -39,9 +102,9 @@ export function generateJunitReport() { .map(([suiteName, cases]) => { const suiteErrors = cases.filter((c) => c.error).length; return ` - <testsuite name="${suiteName}" - tests="${cases.length}" - errors="${suiteErrors}" + <testsuite name="${suiteName}" + tests="${cases.length}" + errors="${suiteErrors}" timestamp="${format(new Date(), "yyyy-MM-dd'T'HH:mm:ss")}"> ${cases .map( @@ -57,6 +120,9 @@ export function generateJunitReport() { </failure>` : "" } + <system-out> + <![CDATA[${formatContext(testCase.context)} + ` ) .join("\n")} From 110152e2c3bec94aeb23cbff877deb4b16a49ad5 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 9 May 2025 00:44:01 +0200 Subject: [PATCH 300/324] CI/CD: Fix? --- .github/workflows/ci.yml | 1 - src/core/database/database.ts | 38 +++++++++++++++++------------------ 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c243ed0..50bbd1b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,7 +51,6 @@ jobs: export DOCKSTATAPI_PORT=5971 export PAD_NEW_LINES=false docker compose -f docker/docker-compose.dev.yaml up -d - bun clean bun test bun clean diff --git a/src/core/database/database.ts b/src/core/database/database.ts index a8d4c13..a471dc8 100644 --- a/src/core/database/database.ts +++ b/src/core/database/database.ts @@ -1,10 +1,10 @@ import { Database } from "bun:sqlite"; - import { existsSync, mkdirSync } from "node:fs"; const dataFolder = "data"; + if (!existsSync(dataFolder)) { - mkdirSync(dataFolder, { recursive: true }); + mkdirSync(dataFolder, { recursive: true }); } const databasePath = "data/dockstatapi.db"; @@ -13,7 +13,7 @@ export const db = new Database(databasePath, { strict: true }); db.exec("PRAGMA journal_mode = WAL;"); export function init() { - db.exec(` + db.exec(` CREATE TABLE IF NOT EXISTS backend_log_entries ( timestamp STRING NOT NULL, level TEXT NOT NULL, @@ -77,25 +77,25 @@ export function init() { ); `); - const configRow = db - .prepare("SELECT COUNT(*) AS count FROM config") - .get() as { count: number }; + const configRow = db + .prepare("SELECT COUNT(*) AS count FROM config") + .get() as { count: number }; - if (configRow.count === 0) { - db.prepare( - 'INSERT INTO config (keep_data_for, fetching_interval, api_key) VALUES (7, 5, "changeme")', - ).run(); - } + if (configRow.count === 0) { + db.prepare( + 'INSERT INTO config (keep_data_for, fetching_interval, api_key) VALUES (7, 5, "changeme")' + ).run(); + } - const hostRow = db - .prepare("SELECT COUNT(*) AS count FROM docker_hosts") - .get() as { count: number }; + const hostRow = db + .prepare("SELECT COUNT(*) AS count FROM docker_hosts") + .get() as { count: number }; - if (hostRow.count === 0) { - db.prepare( - "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)", - ).run("Localhost", "localhost:2375", false); - } + if (hostRow.count === 0) { + db.prepare( + "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)" + ).run("Localhost", "localhost:2375", false); + } } init(); From af1f3e5700bf6511b879a28235deadf4a3bfe09f Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 9 May 2025 00:45:58 +0200 Subject: [PATCH 301/324] CI/CD: Remove different server port in CI --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 50bbd1b..31877af 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,7 +48,6 @@ jobs: - name: Run unit tests run: | - export DOCKSTATAPI_PORT=5971 export PAD_NEW_LINES=false docker compose -f docker/docker-compose.dev.yaml up -d bun test From 343498046510b71008e7b2933e711b7995321329 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 9 May 2025 00:50:07 +0200 Subject: [PATCH 302/324] CI/CD: This? --- src/core/database/database.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/core/database/database.ts b/src/core/database/database.ts index a471dc8..24709fa 100644 --- a/src/core/database/database.ts +++ b/src/core/database/database.ts @@ -7,10 +7,17 @@ if (!existsSync(dataFolder)) { mkdirSync(dataFolder, { recursive: true }); } -const databasePath = "data/dockstatapi.db"; -export const db = new Database(databasePath, { strict: true }); +export let db: Database; -db.exec("PRAGMA journal_mode = WAL;"); +try { + const databasePath = "data/dockstatapi.db"; + db = new Database(databasePath, { strict: true }); + db.exec("PRAGMA journal_mode = WAL;"); +} catch (error) { + console.error(`Cannot start DockStatAPI: ${error}`); + process.exit; + throw new Error(error as string); +} export function init() { db.exec(` From b29081beb4105050adfe2aaeb1d0244e8de5dadb Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 9 May 2025 00:54:38 +0200 Subject: [PATCH 303/324] CI/CD: istg ts works on my machine --- src/core/database/database.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/core/database/database.ts b/src/core/database/database.ts index 24709fa..cf5512e 100644 --- a/src/core/database/database.ts +++ b/src/core/database/database.ts @@ -3,19 +3,19 @@ import { existsSync, mkdirSync } from "node:fs"; const dataFolder = "data"; -if (!existsSync(dataFolder)) { - mkdirSync(dataFolder, { recursive: true }); -} - export let db: Database; try { - const databasePath = "data/dockstatapi.db"; - db = new Database(databasePath, { strict: true }); + const databasePath = `${dataFolder}/dockstatapi.db`; + + if (!existsSync(dataFolder)) { + mkdirSync(dataFolder, { recursive: true }); + } + + db = new Database(databasePath, { strict: true, create: true }); db.exec("PRAGMA journal_mode = WAL;"); } catch (error) { console.error(`Cannot start DockStatAPI: ${error}`); - process.exit; throw new Error(error as string); } From 805fe4ed2df57ccddbfb15f49d98b21975981589 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Sun, 11 May 2025 13:11:59 +0200 Subject: [PATCH 304/324] Feat: More resillient debugging for Database initialisation --- src/core/database/database.ts | 24 ++++++++++++++++++------ src/core/database/dockerHosts.ts | 1 - 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/core/database/database.ts b/src/core/database/database.ts index cf5512e..8375906 100644 --- a/src/core/database/database.ts +++ b/src/core/database/database.ts @@ -1,22 +1,34 @@ import { Database } from "bun:sqlite"; -import { existsSync, mkdirSync } from "node:fs"; +import { existsSync } from "node:fs"; +import { mkdir } from "node:fs/promises"; +import path from "node:path"; +import { userInfo } from "node:os"; -const dataFolder = "data"; +const dataFolder = path.join(process.cwd(), "data"); + +const username = userInfo().username; +const gid = userInfo().gid; +const uid = userInfo().uid; export let db: Database; try { - const databasePath = `${dataFolder}/dockstatapi.db`; + const databasePath = path.join(dataFolder, "dockstatapi.db"); + console.log("Database path:", databasePath); + console.log(`Running as: ${username} (${uid}:${gid})`); if (!existsSync(dataFolder)) { - mkdirSync(dataFolder, { recursive: true }); + await mkdir(dataFolder, { recursive: true, mode: 0o777 }); + console.log("Created data directory:", dataFolder); } - db = new Database(databasePath, { strict: true, create: true }); + db = new Database(databasePath, { create: true }); + console.log("Database opened successfully"); + db.exec("PRAGMA journal_mode = WAL;"); } catch (error) { console.error(`Cannot start DockStatAPI: ${error}`); - throw new Error(error as string); + process.exit(500); } export function init() { diff --git a/src/core/database/dockerHosts.ts b/src/core/database/dockerHosts.ts index a2fc2ca..62b9485 100644 --- a/src/core/database/dockerHosts.ts +++ b/src/core/database/dockerHosts.ts @@ -1,4 +1,3 @@ -import type { DockerHost } from "~/typings/docker"; import { db } from "./database"; import { executeDbOperation } from "./helper"; From d022f7271b0154b4a1fafab2af4d9cbc77a3753f Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Sun, 11 May 2025 11:12:34 +0000 Subject: [PATCH 305/324] Update dependency graphs --- dependency-graph.mmd | 297 ++++----- dependency-graph.svg | 1435 +++++++++++++++++++++--------------------- 2 files changed, 878 insertions(+), 854 deletions(-) diff --git a/dependency-graph.mmd b/dependency-graph.mmd index e54cc0b..8567428 100644 --- a/dependency-graph.mmd +++ b/dependency-graph.mmd @@ -10,55 +10,55 @@ subgraph 0["src"] 1["index.ts"] subgraph 6["routes"] 7["live-stacks.ts"] -V["live-logs.ts"] -1H["api-config.ts"] -1J["docker-manager.ts"] -1K["docker-stats.ts"] -1L["docker-websocket.ts"] -1N["logs.ts"] -1O["stacks.ts"] -1R["utils.ts"] +X["live-logs.ts"] +1I["api-config.ts"] +1K["docker-manager.ts"] +1L["docker-stats.ts"] +1M["docker-websocket.ts"] +1O["logs.ts"] +1P["stacks.ts"] +1S["utils.ts"] end subgraph 9["core"] subgraph A["utils"] B["logger.ts"] -U["helpers.ts"] -15["calculations.ts"] -19["change-me-checker.ts"] -1B["package-json.ts"] -1D["swagger-readme.ts"] -1I["response-handler.ts"] +W["helpers.ts"] +17["calculations.ts"] +1B["change-me-checker.ts"] +1C["package-json.ts"] +1E["swagger-readme.ts"] +1J["response-handler.ts"] end subgraph D["database"] E["_dbState.ts"] F["index.ts"] G["backup.ts"] J["database.ts"] -L["helper.ts"] -M["config.ts"] -N["containerStats.ts"] -O["dockerHosts.ts"] -Q["hostStats.ts"] -R["logs.ts"] -S["stacks.ts"] +N["helper.ts"] +O["config.ts"] +P["containerStats.ts"] +Q["dockerHosts.ts"] +R["hostStats.ts"] +T["logs.ts"] +U["stacks.ts"] end -subgraph W["docker"] -X["monitor.ts"] -12["client.ts"] -13["scheduler.ts"] -14["store-container-stats.ts"] -16["store-host-stats.ts"] +subgraph Y["docker"] +Z["monitor.ts"] +14["client.ts"] +15["scheduler.ts"] +16["store-container-stats.ts"] +18["store-host-stats.ts"] end -subgraph Y["plugins"] -Z["plugin-manager.ts"] -18["loader.ts"] +subgraph 10["plugins"] +11["plugin-manager.ts"] +1A["loader.ts"] end -subgraph 1P["stacks"] -1Q["controller.ts"] +subgraph 1Q["stacks"] +1R["controller.ts"] end end -subgraph 1E["middleware"] -1F["auth.ts"] +subgraph 1F["middleware"] +1G["auth.ts"] end end subgraph 2["~"] @@ -66,167 +66,170 @@ subgraph 3["typings"] 4["database"] 8["websocket"] H["misc"] -P["docker"] -T["docker-compose"] -10["plugin"] -17["dockerode"] -1G["elysiajs"] +S["docker"] +V["docker-compose"] +12["plugin"] +19["dockerode"] +1H["elysiajs"] end end 5["elysia-remote-dts"] C["path"] subgraph I["fs"] -1A["promises"] +L["promises"] end K["bun:sqlite"] -11["events"] -1C["package.json"] -1M["stream"] +M["os"] +13["events"] +1D["package.json"] +1N["stream"] 1-->7 1-->F -1-->X -1-->13 -1-->18 +1-->Z +1-->15 +1-->1A 1-->B -1-->1B -1-->1D -1-->1F -1-->1H -1-->1J +1-->1C +1-->1E +1-->1G +1-->1I 1-->1K 1-->1L -1-->V -1-->1N +1-->1M +1-->X 1-->1O -1-->1R +1-->1P +1-->1S 1-->4 1-->5 7-->B 7-->8 B-->E B-->F -B-->V +B-->X B-->4 B-->C F-->G -F-->M -F-->N -F-->J F-->O +F-->P +F-->J F-->Q F-->R -F-->S +F-->T +F-->U G-->E G-->J -G-->L +G-->N G-->B G-->H G-->I J-->K J-->I -L-->E -L-->B -M-->J -M-->L -N-->J -N-->L +J-->L +J-->M +J-->C +N-->E +N-->B O-->J -O-->L -O-->P +O-->N +P-->J +P-->N Q-->J -Q-->L -Q-->P +Q-->N R-->J -R-->L -R-->4 -S-->U -S-->J -S-->L -S-->4 -S-->T -U-->B -V-->B -V-->4 -X-->Z -X-->F -X-->12 +R-->N +R-->S +T-->J +T-->N +T-->4 +U-->W +U-->J +U-->N +U-->4 +U-->V +W-->B X-->B -X-->P -Z-->B -Z-->P -Z-->10 +X-->4 Z-->11 -12-->B -12-->P -13-->F -13-->14 -13-->16 -13-->B -13-->4 +Z-->F +Z-->14 +Z-->B +Z-->S +11-->B +11-->S +11-->12 +11-->13 14-->B -14-->F -14-->12 -14-->15 -16-->F -16-->12 -16-->U +14-->S +15-->F +15-->16 +15-->18 +15-->B +15-->4 16-->B -16-->P +16-->F +16-->14 16-->17 -18-->19 +18-->F +18-->14 +18-->W 18-->B -18-->Z -18-->I -18-->C -19-->B -19-->1A -1B-->1C -1F-->F -1F-->B -1F-->4 -1F-->1G -1H-->F -1H-->G -1H-->Z -1H-->B -1H-->1B -1H-->1I -1H-->1F -1H-->4 -1H-->I +18-->S +18-->19 +1A-->1B +1A-->B +1A-->11 +1A-->I +1A-->C +1B-->B +1B-->L +1C-->1D +1G-->F +1G-->B +1G-->4 +1G-->1H +1I-->F +1I-->G +1I-->11 1I-->B +1I-->1C +1I-->1J 1I-->1G -1J-->F +1I-->4 +1I-->I 1J-->B -1J-->1I -1J-->P +1J-->1H 1K-->F -1K-->12 -1K-->15 -1K-->U 1K-->B -1K-->1I -1K-->P -1K-->17 +1K-->1J +1K-->S 1L-->F -1L-->12 -1L-->15 +1L-->14 +1L-->17 +1L-->W 1L-->B -1L-->1I -1L-->1M -1N-->F -1N-->B +1L-->1J +1L-->S +1L-->19 +1M-->F +1M-->14 +1M-->17 +1M-->B +1M-->1J +1M-->1N 1O-->F -1O-->1Q 1O-->B -1O-->1I -1Q-->U -1Q-->F -1Q-->B -1Q-->7 -1Q-->4 -1Q-->T -1Q-->1A -1R-->1B -1R-->1I +1P-->F +1P-->1R +1P-->B +1P-->1J +1R-->W +1R-->F +1R-->B +1R-->7 +1R-->4 +1R-->V +1R-->L +1S-->1C +1S-->1J diff --git a/dependency-graph.svg b/dependency-graph.svg index 6e3dd9e..abb59e5 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -4,77 +4,77 @@ - - + + dependency-cruiser output - + cluster_fs - -fs + +fs cluster_src - -src + +src cluster_src/core - -core + +core cluster_src/core/database - -database + +database cluster_src/core/docker - -docker + +docker cluster_src/core/plugins - -plugins + +plugins cluster_src/core/stacks - -stacks + +stacks cluster_src/core/utils - -utils + +utils cluster_src/middleware - -middleware + +middleware cluster_src/routes - -routes + +routes cluster_~ - -~ + +~ cluster_~/typings - -typings + +typings bun:sqlite - -bun:sqlite + +bun:sqlite @@ -82,8 +82,8 @@ elysia-remote-dts - -elysia-remote-dts + +elysia-remote-dts @@ -91,8 +91,8 @@ events - -events + +events @@ -100,8 +100,8 @@ fs - -fs + +fs @@ -109,1366 +109,1387 @@ fs/promises - -promises + +promises - + +os + + +os + + + + + package.json - - -package.json + + +package.json - + path - - -path + + +path - + src/core/database/_dbState.ts - - -_dbState.ts + + +_dbState.ts - + src/core/database/backup.ts - - -backup.ts + + +backup.ts src/core/database/backup.ts->fs - - + + src/core/database/backup.ts->src/core/database/_dbState.ts - - + + - + src/core/database/database.ts - - -database.ts + + +database.ts src/core/database/backup.ts->src/core/database/database.ts - - + + - + src/core/database/helper.ts - - -helper.ts + + +helper.ts src/core/database/backup.ts->src/core/database/helper.ts - - - - + + + + - + src/core/utils/logger.ts - - -logger.ts + + +logger.ts src/core/database/backup.ts->src/core/utils/logger.ts - - - - + + + + - + ~/typings/misc - - -misc + + +misc src/core/database/backup.ts->~/typings/misc - - + + src/core/database/database.ts->bun:sqlite - - + + src/core/database/database.ts->fs - - + + + + + +src/core/database/database.ts->fs/promises + + + + + +src/core/database/database.ts->os + + + + + +src/core/database/database.ts->path + + - + src/core/database/helper.ts->src/core/database/_dbState.ts - - + + - + src/core/database/helper.ts->src/core/utils/logger.ts - - - - + + + + - + src/core/utils/logger.ts->path - - + + - + src/core/utils/logger.ts->src/core/database/_dbState.ts - - + + - + src/core/database/index.ts - - -index.ts + + +index.ts - + src/core/utils/logger.ts->src/core/database/index.ts - - - - + + + + - + ~/typings/database - - -database + + +database - + src/core/utils/logger.ts->~/typings/database - - + + - + src/routes/live-logs.ts - - -live-logs.ts + + +live-logs.ts - + src/core/utils/logger.ts->src/routes/live-logs.ts - - - - + + + + - + src/core/database/config.ts - - -config.ts + + +config.ts src/core/database/config.ts->src/core/database/database.ts - - + + src/core/database/config.ts->src/core/database/helper.ts - - - - + + + + - + src/core/database/containerStats.ts - - -containerStats.ts + + +containerStats.ts src/core/database/containerStats.ts->src/core/database/database.ts - - + + src/core/database/containerStats.ts->src/core/database/helper.ts - - - - + + + + - + src/core/database/dockerHosts.ts - - -dockerHosts.ts + + +dockerHosts.ts - + src/core/database/dockerHosts.ts->src/core/database/database.ts - - + + - + src/core/database/dockerHosts.ts->src/core/database/helper.ts - - - - - - - -~/typings/docker - - -docker - - - - - -src/core/database/dockerHosts.ts->~/typings/docker - - + + + + src/core/database/hostStats.ts - -hostStats.ts + +hostStats.ts - + src/core/database/hostStats.ts->src/core/database/database.ts - - + + - + src/core/database/hostStats.ts->src/core/database/helper.ts - - - - + + + + + + + +~/typings/docker + + +docker + + - + src/core/database/hostStats.ts->~/typings/docker - - + + - + src/core/database/index.ts->src/core/database/backup.ts - - - - + + + + - + src/core/database/index.ts->src/core/database/database.ts - - + + - + src/core/database/index.ts->src/core/database/config.ts - - - - + + + + - + src/core/database/index.ts->src/core/database/containerStats.ts - - - - + + + + - + src/core/database/index.ts->src/core/database/dockerHosts.ts - - - - + + + + - + src/core/database/index.ts->src/core/database/hostStats.ts - - - - + + + + - + src/core/database/logs.ts - - -logs.ts + + +logs.ts - + src/core/database/index.ts->src/core/database/logs.ts - - - - + + + + - + src/core/database/stacks.ts - - -stacks.ts + + +stacks.ts - + src/core/database/index.ts->src/core/database/stacks.ts - - - - + + + + - + src/core/database/logs.ts->src/core/database/database.ts - - + + - + src/core/database/logs.ts->src/core/database/helper.ts - - - - + + + + - + src/core/database/logs.ts->~/typings/database - - + + - + src/core/database/stacks.ts->src/core/database/database.ts - - + + - + src/core/database/stacks.ts->src/core/database/helper.ts - - - - + + + + - + src/core/database/stacks.ts->~/typings/database - - + + - + src/core/utils/helpers.ts - - -helpers.ts + + +helpers.ts - + src/core/database/stacks.ts->src/core/utils/helpers.ts - - - - + + + + - + ~/typings/docker-compose - - -docker-compose + + +docker-compose - + src/core/database/stacks.ts->~/typings/docker-compose - - + + - + src/core/utils/helpers.ts->src/core/utils/logger.ts - - - - + + + + - + src/core/docker/client.ts - - -client.ts + + +client.ts - + src/core/docker/client.ts->src/core/utils/logger.ts - - + + - + src/core/docker/client.ts->~/typings/docker - - + + - + src/core/docker/monitor.ts - - -monitor.ts + + +monitor.ts - + src/core/docker/monitor.ts->src/core/utils/logger.ts - - + + - + src/core/docker/monitor.ts->~/typings/docker - - + + - + src/core/docker/monitor.ts->src/core/database/index.ts - - + + - + src/core/docker/monitor.ts->src/core/docker/client.ts - - + + - + src/core/plugins/plugin-manager.ts - - -plugin-manager.ts + + +plugin-manager.ts - + src/core/docker/monitor.ts->src/core/plugins/plugin-manager.ts - - + + - + src/core/plugins/plugin-manager.ts->events - - + + - + src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts - - + + - + src/core/plugins/plugin-manager.ts->~/typings/docker - - + + - + ~/typings/plugin - - -plugin + + +plugin - + src/core/plugins/plugin-manager.ts->~/typings/plugin - - + + - + src/core/docker/scheduler.ts - - -scheduler.ts + + +scheduler.ts - + src/core/docker/scheduler.ts->src/core/utils/logger.ts - - + + - + src/core/docker/scheduler.ts->src/core/database/index.ts - - + + - + src/core/docker/scheduler.ts->~/typings/database - - + + - + src/core/docker/store-container-stats.ts - - -store-container-stats.ts + + +store-container-stats.ts - + src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts - - + + - + src/core/docker/store-host-stats.ts - - -store-host-stats.ts + + +store-host-stats.ts - + src/core/docker/scheduler.ts->src/core/docker/store-host-stats.ts - - + + - + src/core/docker/store-container-stats.ts->src/core/utils/logger.ts - - + + - + src/core/docker/store-container-stats.ts->src/core/database/index.ts - - + + - + src/core/docker/store-container-stats.ts->src/core/docker/client.ts - - + + - + src/core/utils/calculations.ts - - -calculations.ts + + +calculations.ts - + src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/utils/logger.ts - - + + - + src/core/docker/store-host-stats.ts->~/typings/docker - - + + - + src/core/docker/store-host-stats.ts->src/core/database/index.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/utils/helpers.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/docker/client.ts - - + + - + ~/typings/dockerode - - -dockerode + + +dockerode - + src/core/docker/store-host-stats.ts->~/typings/dockerode - - + + - + src/core/plugins/loader.ts - - -loader.ts + + +loader.ts - + src/core/plugins/loader.ts->fs - - + + - + src/core/plugins/loader.ts->path - - + + - + src/core/plugins/loader.ts->src/core/utils/logger.ts - - + + - + src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts - - + + - + src/core/utils/change-me-checker.ts - - -change-me-checker.ts + + +change-me-checker.ts - + src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts - - + + - + src/core/utils/change-me-checker.ts->fs/promises - - + + - + src/core/utils/change-me-checker.ts->src/core/utils/logger.ts - - + + - + src/core/stacks/controller.ts - - -controller.ts + + +controller.ts - + src/core/stacks/controller.ts->fs/promises - - + + - + src/core/stacks/controller.ts->src/core/utils/logger.ts - - + + - + src/core/stacks/controller.ts->src/core/database/index.ts - - + + - + src/core/stacks/controller.ts->~/typings/database - - + + - + src/core/stacks/controller.ts->src/core/utils/helpers.ts - - + + - + src/core/stacks/controller.ts->~/typings/docker-compose - - + + - + src/routes/live-stacks.ts - - -live-stacks.ts + + +live-stacks.ts - + src/core/stacks/controller.ts->src/routes/live-stacks.ts - - + + - + src/routes/live-stacks.ts->src/core/utils/logger.ts - - + + - + ~/typings/websocket - - -websocket + + +websocket - + src/routes/live-stacks.ts->~/typings/websocket - - + + - + src/routes/live-logs.ts->src/core/utils/logger.ts - - - - + + + + - + src/routes/live-logs.ts->~/typings/database - - + + - + src/core/utils/package-json.ts - - -package-json.ts + + +package-json.ts - + src/core/utils/package-json.ts->package.json - - + + - + src/core/utils/response-handler.ts - - -response-handler.ts + + +response-handler.ts - + src/core/utils/response-handler.ts->src/core/utils/logger.ts - - + + - + ~/typings/elysiajs - - -elysiajs + + +elysiajs - + src/core/utils/response-handler.ts->~/typings/elysiajs - - + + - + src/core/utils/swagger-readme.ts - - -swagger-readme.ts + + +swagger-readme.ts - + src/index.ts - - -index.ts + + +index.ts - + src/index.ts->elysia-remote-dts - - + + - + src/index.ts->src/core/utils/logger.ts - - + + - + src/index.ts->src/core/database/index.ts - - + + - + src/index.ts->~/typings/database - - + + - + src/index.ts->src/core/docker/monitor.ts - - + + - + src/index.ts->src/core/docker/scheduler.ts - - + + - + src/index.ts->src/core/plugins/loader.ts - - + + - + src/index.ts->src/routes/live-stacks.ts - - + + - + src/index.ts->src/routes/live-logs.ts - - + + - + src/index.ts->src/core/utils/package-json.ts - - + + - + src/index.ts->src/core/utils/swagger-readme.ts - - + + - + src/middleware/auth.ts - - -auth.ts + + +auth.ts - + src/index.ts->src/middleware/auth.ts - - + + - + src/routes/api-config.ts - - -api-config.ts + + +api-config.ts - + src/index.ts->src/routes/api-config.ts - - + + - + src/routes/docker-manager.ts - - -docker-manager.ts + + +docker-manager.ts - + src/index.ts->src/routes/docker-manager.ts - - + + - + src/routes/docker-stats.ts - - -docker-stats.ts + + +docker-stats.ts - + src/index.ts->src/routes/docker-stats.ts - - + + - + src/routes/docker-websocket.ts - - -docker-websocket.ts + + +docker-websocket.ts - + src/index.ts->src/routes/docker-websocket.ts - - + + - + src/routes/logs.ts - - -logs.ts + + +logs.ts - + src/index.ts->src/routes/logs.ts - - + + - + src/routes/stacks.ts - - -stacks.ts + + +stacks.ts - + src/index.ts->src/routes/stacks.ts - - + + - + src/routes/utils.ts - - -utils.ts + + +utils.ts - + src/index.ts->src/routes/utils.ts - - + + - + src/middleware/auth.ts->src/core/utils/logger.ts - - + + - + src/middleware/auth.ts->src/core/database/index.ts - - + + - + src/middleware/auth.ts->~/typings/database - - + + - + src/middleware/auth.ts->~/typings/elysiajs - - + + - + src/routes/api-config.ts->fs - - + + - + src/routes/api-config.ts->src/core/database/backup.ts - - + + - + src/routes/api-config.ts->src/core/utils/logger.ts - - + + - + src/routes/api-config.ts->src/core/database/index.ts - - + + - + src/routes/api-config.ts->~/typings/database - - + + - + src/routes/api-config.ts->src/core/plugins/plugin-manager.ts - - + + - + src/routes/api-config.ts->src/core/utils/package-json.ts - - + + - + src/routes/api-config.ts->src/core/utils/response-handler.ts - - + + - + src/routes/api-config.ts->src/middleware/auth.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-manager.ts->~/typings/docker - - + + - + src/routes/docker-manager.ts->src/core/database/index.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/response-handler.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-stats.ts->~/typings/docker - - + + - + src/routes/docker-stats.ts->src/core/database/index.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/helpers.ts - - + + - + src/routes/docker-stats.ts->src/core/docker/client.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-stats.ts->~/typings/dockerode - - + + - + src/routes/docker-stats.ts->src/core/utils/response-handler.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-websocket.ts->src/core/database/index.ts - - + + - + src/routes/docker-websocket.ts->src/core/docker/client.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/response-handler.ts - - + + - + stream - - -stream + + +stream - + src/routes/docker-websocket.ts->stream - - + + - + src/routes/logs.ts->src/core/utils/logger.ts - - + + - + src/routes/logs.ts->src/core/database/index.ts - - + + - + src/routes/stacks.ts->src/core/utils/logger.ts - - + + - + src/routes/stacks.ts->src/core/database/index.ts - - + + - + src/routes/stacks.ts->src/core/stacks/controller.ts - - + + - + src/routes/stacks.ts->src/core/utils/response-handler.ts - - + + - + src/routes/utils.ts->src/core/utils/package-json.ts - - + + - + src/routes/utils.ts->src/core/utils/response-handler.ts - - + + From e8c7c2a82ca610eb602bddc1b952baebc3e0bbfe Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 14 May 2025 16:20:03 +0200 Subject: [PATCH 306/324] Feat: Better request logging (not saved in db), adjusted error responses, adjusted stack_config, more debug logging, typo fix --- .local-tests/stacks.md | 40 -- bun.lock | 444 ++++++++++++ package.json | 1 + src/core/database/database.ts | 71 +- src/core/database/dockerHosts.ts | 88 +-- src/core/database/index.ts | 14 +- src/core/database/stacks.ts | 23 +- src/core/docker/store-container-stats.ts | 2 +- src/core/stacks/controller.ts | 652 +++++++++-------- src/core/utils/helpers.ts | 4 +- src/index.ts | 26 +- src/middleware/auth.ts | 17 +- src/routes/api-config.ts | 66 +- src/routes/live-stacks.ts | 35 +- src/routes/stacks.ts | 77 +- src/routes/utils.ts | 123 ---- src/tests/api-config.spec.ts | 634 ++++++++--------- src/tests/docker-manager.spec.ts | 866 +++++++++++------------ tsconfig.json | 4 +- 19 files changed, 1753 insertions(+), 1434 deletions(-) delete mode 100644 .local-tests/stacks.md create mode 100644 bun.lock diff --git a/.local-tests/stacks.md b/.local-tests/stacks.md deleted file mode 100644 index 22a2c51..0000000 --- a/.local-tests/stacks.md +++ /dev/null @@ -1,40 +0,0 @@ -# Testing Stacks - -## Deployment - -### Values - -- compose_spec -- name -- version -- automatic_reboot_on_error -- isCustom -- image_updates -- source -- stack_prefix - -### JSON - -```json -{ - "compose_spec": { - "name": "Local Test", - "services": { - "nginx": { - "container_name": "Local-test-nginx", - "image": "dockerbogo/docker-nginx-hello-world", - "ports": [ - "8081:80" - ] - } - } - }, - "name": "Local-Test", - "version": 1, - "automatic_reboot_on_error": true, - "isCustom": true, - "image_updates": true, - "source": "Local", - "stack_prefix": "" -} -``` diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..f2cc38f --- /dev/null +++ b/bun.lock @@ -0,0 +1,444 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "dockstatapi", + "dependencies": { + "@elysiajs/server-timing": "^1.3.0", + "@elysiajs/static": "^1.3.0", + "@elysiajs/swagger": "^1.3.0", + "chalk": "^5.4.1", + "date-fns": "^4.1.0", + "docker-compose": "^1.2.0", + "dockerode": "^4.0.6", + "elysia": "latest", + "elysia-remote-dts": "^1.0.2", + "knip": "latest", + "logestic": "^1.2.4", + "split2": "^4.2.0", + "winston": "^3.17.0", + "yaml": "^2.7.1", + }, + "devDependencies": { + "@biomejs/biome": "1.9.4", + "@types/bun": "latest", + "@types/dockerode": "^3.3.38", + "@types/node": "^22.15.17", + "@types/split2": "^4.2.3", + "bun-types": "latest", + "cross-env": "^7.0.3", + "logform": "^2.7.0", + "typescript": "^5.8.3", + "wrap-ansi": "^9.0.0", + }, + }, + }, + "trustedDependencies": [ + "protobufjs", + ], + "packages": { + "@balena/dockerignore": ["@balena/dockerignore@1.0.2", "", {}, "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q=="], + + "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="], + + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="], + + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="], + + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="], + + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="], + + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="], + + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="], + + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="], + + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], + + "@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="], + + "@dabh/diagnostics": ["@dabh/diagnostics@2.0.3", "", { "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA=="], + + "@elysiajs/server-timing": ["@elysiajs/server-timing@1.3.0", "", { "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-c5Ay0Va7gIWjJ9CawHx05UtKP6UQVkMKCFnf16eBG0G/GgUkrMMGHWD/duCBaDbeRwbbb7IwHDoaFvStWrB2IQ=="], + + "@elysiajs/static": ["@elysiajs/static@1.3.0", "", { "dependencies": { "node-cache": "^5.1.2" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-7mWlj2U/AZvH27IfRKqpUjDP1W9ZRldF9NmdnatFEtx0AOy7YYgyk0rt5hXrH6wPcR//2gO2Qy+k5rwswpEhJA=="], + + "@elysiajs/swagger": ["@elysiajs/swagger@1.3.0", "", { "dependencies": { "@scalar/themes": "^0.9.52", "@scalar/types": "^0.0.12", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-0fo3FWkDRPNYpowJvLz3jBHe9bFe6gruZUyf+feKvUEEMG9ZHptO1jolSoPE0ffFw1BgN1/wMsP19p4GRXKdfg=="], + + "@grpc/grpc-js": ["@grpc/grpc-js@1.13.3", "", { "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-FTXHdOoPbZrBjlVLHuKbDZnsTxXv2BlHF57xw6LuThXacXvtkahEPED0CKMk6obZDf65Hv4k3z62eyPNpvinIg=="], + + "@grpc/proto-loader": ["@grpc/proto-loader@0.7.15", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.2.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ=="], + + "@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], + + "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], + + "@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="], + + "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="], + + "@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="], + + "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], + + "@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="], + + "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], + + "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], + + "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="], + + "@scalar/openapi-types": ["@scalar/openapi-types@0.1.1", "", {}, "sha512-NMy3QNk6ytcCoPUGJH0t4NNr36OWXgZhA3ormr3TvhX1NDgoF95wFyodGVH8xiHeUyn2/FxtETm8UBLbB5xEmg=="], + + "@scalar/themes": ["@scalar/themes@0.9.86", "", { "dependencies": { "@scalar/types": "0.1.7" } }, "sha512-QUHo9g5oSWi+0Lm1vJY9TaMZRau8LHg+vte7q5BVTBnu6NuQfigCaN+ouQ73FqIVd96TwMO6Db+dilK1B+9row=="], + + "@scalar/types": ["@scalar/types@0.0.12", "", { "dependencies": { "@scalar/openapi-types": "0.1.1", "@unhead/schema": "^1.9.5" } }, "sha512-XYZ36lSEx87i4gDqopQlGCOkdIITHHEvgkuJFrXFATQs9zHARop0PN0g4RZYWj+ZpCUclOcaOjbCt8JGe22mnQ=="], + + "@sinclair/typebox": ["@sinclair/typebox@0.34.33", "", {}, "sha512-5HAV9exOMcXRUxo+9iYB5n09XxzCXnfy4VTNW4xnDv+FgjzAGY989C28BIdljKqmF+ZltUwujE3aossvcVtq6g=="], + + "@tokenizer/inflate": ["@tokenizer/inflate@0.2.7", "", { "dependencies": { "debug": "^4.4.0", "fflate": "^0.8.2", "token-types": "^6.0.0" } }, "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg=="], + + "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], + + "@types/bun": ["@types/bun@1.2.13", "", { "dependencies": { "bun-types": "1.2.13" } }, "sha512-u6vXep/i9VBxoJl3GjZsl/BFIsvML8DfVDO0RYLEwtSZSp981kEO1V5NwRcO1CPJ7AmvpbnDCiMKo3JvbDEjAg=="], + + "@types/docker-modem": ["@types/docker-modem@3.0.6", "", { "dependencies": { "@types/node": "*", "@types/ssh2": "*" } }, "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg=="], + + "@types/dockerode": ["@types/dockerode@3.3.38", "", { "dependencies": { "@types/docker-modem": "*", "@types/node": "*", "@types/ssh2": "*" } }, "sha512-nnrcfUe2iR+RyOuz0B4bZgQwD9djQa9ADEjp7OAgBs10pYT0KSCtplJjcmBDJz0qaReX5T7GbE5i4VplvzUHvA=="], + + "@types/node": ["@types/node@22.15.17", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw=="], + + "@types/split2": ["@types/split2@4.2.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-59OXIlfUsi2k++H6CHgUQKEb2HKRokUA39HY1i1dS8/AIcqVjtAAFdf8u+HxTWK/4FUHMJQlKSZ4I6irCBJ1Zw=="], + + "@types/ssh2": ["@types/ssh2@1.15.5", "", { "dependencies": { "@types/node": "^18.11.18" } }, "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ=="], + + "@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="], + + "@unhead/schema": ["@unhead/schema@1.11.20", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA=="], + + "ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + + "ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="], + + "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="], + + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + + "buildcheck": ["buildcheck@0.0.6", "", {}, "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A=="], + + "bun-types": ["bun-types@1.2.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-rRjA1T6n7wto4gxhAO/ErZEtOXyEZEmnIHQfl0Dt1QQSB4QV0iP6BZ9/YB5fZaHFQ2dwHFrmPaRQ9GGMX01k9Q=="], + + "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], + + "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], + + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="], + + "color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="], + + "color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + + "color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + + "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], + + "colorspace": ["colorspace@1.1.4", "", { "dependencies": { "color": "^3.1.3", "text-hex": "1.0.x" } }, "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w=="], + + "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], + + "cpu-features": ["cpu-features@0.0.10", "", { "dependencies": { "buildcheck": "~0.0.6", "nan": "^2.19.0" } }, "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA=="], + + "cross-env": ["cross-env@7.0.3", "", { "dependencies": { "cross-spawn": "^7.0.1" }, "bin": { "cross-env": "src/bin/cross-env.js", "cross-env-shell": "src/bin/cross-env-shell.js" } }, "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="], + + "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "docker-compose": ["docker-compose@1.2.0", "", { "dependencies": { "yaml": "^2.2.2" } }, "sha512-wIU1eHk3Op7dFgELRdmOYlPYS4gP8HhH1ZmZa13QZF59y0fblzFDFmKPhyc05phCy2hze9OEvNZAsoljrs+72w=="], + + "docker-modem": ["docker-modem@5.0.6", "", { "dependencies": { "debug": "^4.1.1", "readable-stream": "^3.5.0", "split-ca": "^1.0.1", "ssh2": "^1.15.0" } }, "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ=="], + + "dockerode": ["dockerode@4.0.6", "", { "dependencies": { "@balena/dockerignore": "^1.0.2", "@grpc/grpc-js": "^1.11.1", "@grpc/proto-loader": "^0.7.13", "docker-modem": "^5.0.6", "protobufjs": "^7.3.2", "tar-fs": "~2.1.2", "uuid": "^10.0.0" } }, "sha512-FbVf3Z8fY/kALB9s+P9epCpWhfi/r0N2DgYYcYpsAUlaTxPjdsitsFobnltb+lyCgAIvf9C+4PSWlTnHlJMf1w=="], + + "elysia": ["elysia@1.3.1", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.1.2", "fast-decode-uri-component": "^1.0.1" }, "optionalDependencies": { "@sinclair/typebox": "^0.34.33", "openapi-types": "^12.1.3" }, "peerDependencies": { "file-type": ">= 20.0.0", "typescript": ">= 5.0.0" } }, "sha512-En41P6cDHcHtQ0nvfsn9ayB+8ahQJqG1nzvPX8FVZjOriFK/RtZPQBtXMfZDq/AsVIk7JFZGFEtAVEmztNJVhQ=="], + + "elysia-remote-dts": ["elysia-remote-dts@1.0.2", "", { "dependencies": { "debug": "4.4.0", "get-tsconfig": "4.10.0" }, "peerDependencies": { "elysia": ">= 1.0.0", "typescript": ">=5" } }, "sha512-ktRxKGozPDW24d3xbUS2sMLNsRHHX/a4Pgqyzv2O0X4HsDrD+agoUYL/PvYQrGJKPSc3xzvU5uvhNHFhEql6aw=="], + + "emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], + + "enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="], + + "end-of-stream": ["end-of-stream@1.4.4", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q=="], + + "enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "exact-mirror": ["exact-mirror@0.1.2", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-wFCPCDLmHbKGUb8TOi/IS7jLsgR8WVDGtDK3CzcB4Guf/weq7G+I+DkXiRSZfbemBFOxOINKpraM6ml78vo8Zw=="], + + "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], + + "fd-package-json": ["fd-package-json@1.2.0", "", { "dependencies": { "walk-up-path": "^3.0.1" } }, "sha512-45LSPmWf+gC5tdCQMNH4s9Sr00bIkiD9aN7dc5hqkrEw1geRYyDQS1v1oMHAW3ysfxfndqGsrDREHHjNNbKUfA=="], + + "fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="], + + "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], + + "file-type": ["file-type@20.5.0", "", { "dependencies": { "@tokenizer/inflate": "^0.2.6", "strtok3": "^10.2.0", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" } }, "sha512-BfHZtG/l9iMm4Ecianu7P8HRD2tBHLtjXinm4X62XBOYzi7CYA7jyqfJzOvXHqzVrVPYqBo2/GvbARMaaJkKVg=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="], + + "formatly": ["formatly@0.2.3", "", { "dependencies": { "fd-package-json": "^1.2.0" }, "bin": { "formatly": "bin/index.mjs" } }, "sha512-WH01vbXEjh9L3bqn5V620xUAWs32CmK4IzWRRY6ep5zpa/mrisL4d9+pRVuETORVDTQw8OycSO1WC68PL51RaA=="], + + "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="], + + "get-tsconfig": ["get-tsconfig@4.10.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A=="], + + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="], + + "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], + + "knip": ["knip@5.55.1", "", { "dependencies": { "@nodelib/fs.walk": "^1.2.3", "enhanced-resolve": "^5.18.1", "fast-glob": "^3.3.3", "formatly": "^0.2.3", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "minimist": "^1.2.8", "picocolors": "^1.1.0", "picomatch": "^4.0.1", "smol-toml": "^1.3.1", "strip-json-comments": "5.0.1", "zod": "^3.22.4", "zod-validation-error": "^3.0.3" }, "peerDependencies": { "@types/node": ">=18", "typescript": ">=5.0.4" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-NYXjgGrXgMdabUKCP2TlBH/e83m9KnLc1VLyWHUtoRrCEJ/C15YtbafrpTvm3td+jE4VdDPgudvXT1IMtCx8lw=="], + + "kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="], + + "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], + + "logestic": ["logestic@1.2.4", "", { "dependencies": { "chalk": "^5.3.0" }, "peerDependencies": { "elysia": "^1.1.3", "typescript": "^5.0.0" } }, "sha512-Wka/xFdKgqU6JBk8yxAUsqcUjPA/aExpcnm7KnOAxlLo1U71kuWGeEjPw8XVLZzLleTWwmRqJUb2yI5XZP+vAA=="], + + "logform": ["logform@2.7.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="], + + "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nan": ["nan@2.22.2", "", {}, "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ=="], + + "nanoid": ["nanoid@5.1.5", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw=="], + + "node-cache": ["node-cache@5.1.2", "", { "dependencies": { "clone": "2.x" } }, "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="], + + "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], + + "peek-readable": ["peek-readable@7.0.0", "", {}, "sha512-nri2TO5JE3/mRryik9LlHFT53cgHfRK0Lt0BAZQXku/AW3E6XLt2GaY8siWi7dvW/m1z0ecn+J+bpDa9ZN3IsQ=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], + + "protobufjs": ["protobufjs@7.5.1", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-3qx3IRjR9WPQKagdwrKjO3Gu8RgQR2qqw+1KnigWhoVjFqegIj1K3bP11sGqhxrO46/XL7lekuG4jmjL+4cLsw=="], + + "pump": ["pump@3.0.2", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], + + "smol-toml": ["smol-toml@1.3.4", "", {}, "sha512-UOPtVuYkzYGee0Bd2Szz8d2G3RfMfJ2t3qVdZUAozZyAk+a0Sxa+QKix0YCwjL/A1RR0ar44nCxaoN9FxdJGwA=="], + + "split-ca": ["split-ca@1.0.1", "", {}, "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ=="], + + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + + "ssh2": ["ssh2@1.16.0", "", { "dependencies": { "asn1": "^0.2.6", "bcrypt-pbkdf": "^1.0.2" }, "optionalDependencies": { "cpu-features": "~0.0.10", "nan": "^2.20.0" } }, "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg=="], + + "stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="], + + "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + + "strip-json-comments": ["strip-json-comments@5.0.1", "", {}, "sha512-0fk9zBqO67Nq5M/m45qHCJxylV/DhBlIOVExqgOMiCCrzrhU6tCibRXNqE3jwJLftzE9SNuZtYbpzcO+i9FiKw=="], + + "strtok3": ["strtok3@10.2.2", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^7.0.0" } }, "sha512-Xt18+h4s7Z8xyZ0tmBoRmzxcop97R4BAh+dXouUDCYn+Em+1P3qpkUfI5ueWLT8ynC5hZ+q4iPEmGG1urvQGBg=="], + + "tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="], + + "tar-fs": ["tar-fs@2.1.2", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA=="], + + "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + + "text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "token-types": ["token-types@6.0.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA=="], + + "triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="], + + "tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="], + + "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], + + "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + + "uint8array-extras": ["uint8array-extras@1.4.0", "", {}, "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], + + "walk-up-path": ["walk-up-path@3.0.1", "", {}, "sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "winston": ["winston@3.17.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw=="], + + "winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="], + + "wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yaml": ["yaml@2.7.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ=="], + + "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "zhead": ["zhead@2.2.4", "", {}, "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag=="], + + "zod": ["zod@3.24.4", "", {}, "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg=="], + + "zod-validation-error": ["zod-validation-error@3.4.1", "", { "peerDependencies": { "zod": "^3.24.4" } }, "sha512-1KP64yqDPQ3rupxNv7oXhf7KdhHHgaqbKuspVoiN93TT0xrBjql+Svjkdjq/Qh/7GSMmgQs3AfvBT0heE35thw=="], + + "@scalar/themes/@scalar/types": ["@scalar/types@0.1.7", "", { "dependencies": { "@scalar/openapi-types": "0.2.0", "@unhead/schema": "^1.11.11", "nanoid": "^5.1.5", "type-fest": "^4.20.0", "zod": "^3.23.8" } }, "sha512-irIDYzTQG2KLvFbuTI8k2Pz/R4JR+zUUSykVTbEMatkzMmVFnn1VzNSMlODbadycwZunbnL2tA27AXed9URVjw=="], + + "@types/ssh2/@types/node": ["@types/node@18.19.100", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-ojmMP8SZBKprc3qGrGk8Ujpo80AXkrP7G2tOT4VWr5jlr5DHjsJF+emXJz+Wm0glmy4Js62oKMdZZ6B9Y+tEcA=="], + + "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "color-string/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.2.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA=="], + + "@types/ssh2/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "cliui/wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "cliui/wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + } +} diff --git a/package.json b/package.json index 8fb4fe5..15ffaf2 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "elysia": "latest", "elysia-remote-dts": "^1.0.2", "knip": "latest", + "logestic": "^1.2.4", "split2": "^4.2.0", "winston": "^3.17.0", "yaml": "^2.7.1" diff --git a/src/core/database/database.ts b/src/core/database/database.ts index 8375906..f8de7cb 100644 --- a/src/core/database/database.ts +++ b/src/core/database/database.ts @@ -1,8 +1,8 @@ import { Database } from "bun:sqlite"; import { existsSync } from "node:fs"; import { mkdir } from "node:fs/promises"; -import path from "node:path"; import { userInfo } from "node:os"; +import path from "node:path"; const dataFolder = path.join(process.cwd(), "data"); @@ -13,26 +13,26 @@ const uid = userInfo().uid; export let db: Database; try { - const databasePath = path.join(dataFolder, "dockstatapi.db"); - console.log("Database path:", databasePath); - console.log(`Running as: ${username} (${uid}:${gid})`); + const databasePath = path.join(dataFolder, "dockstatapi.db"); + console.log("Database path:", databasePath); + console.log(`Running as: ${username} (${uid}:${gid})`); - if (!existsSync(dataFolder)) { - await mkdir(dataFolder, { recursive: true, mode: 0o777 }); - console.log("Created data directory:", dataFolder); - } + if (!existsSync(dataFolder)) { + await mkdir(dataFolder, { recursive: true, mode: 0o777 }); + console.log("Created data directory:", dataFolder); + } - db = new Database(databasePath, { create: true }); - console.log("Database opened successfully"); + db = new Database(databasePath, { create: true }); + console.log("Database opened successfully"); - db.exec("PRAGMA journal_mode = WAL;"); + db.exec("PRAGMA journal_mode = WAL;"); } catch (error) { - console.error(`Cannot start DockStatAPI: ${error}`); - process.exit(500); + console.error(`Cannot start DockStatAPI: ${error}`); + process.exit(500); } export function init() { - db.exec(` + db.exec(` CREATE TABLE IF NOT EXISTS backend_log_entries ( timestamp STRING NOT NULL, level TEXT NOT NULL, @@ -47,10 +47,7 @@ export function init() { version INTEGER NOT NULL, custom BOOLEAN NOT NULL, source TEXT NOT NULL, - container_count INTEGER NOT NULL, - stack_prefix TEXT NOT NULL, - automatic_reboot_on_error BOOLEAN NOT NULL, - image_updates BOOLEAN NOT NULL + compose_spec TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS docker_hosts ( @@ -96,25 +93,25 @@ export function init() { ); `); - const configRow = db - .prepare("SELECT COUNT(*) AS count FROM config") - .get() as { count: number }; - - if (configRow.count === 0) { - db.prepare( - 'INSERT INTO config (keep_data_for, fetching_interval, api_key) VALUES (7, 5, "changeme")' - ).run(); - } - - const hostRow = db - .prepare("SELECT COUNT(*) AS count FROM docker_hosts") - .get() as { count: number }; - - if (hostRow.count === 0) { - db.prepare( - "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)" - ).run("Localhost", "localhost:2375", false); - } + const configRow = db + .prepare("SELECT COUNT(*) AS count FROM config") + .get() as { count: number }; + + if (configRow.count === 0) { + db.prepare( + 'INSERT INTO config (keep_data_for, fetching_interval, api_key) VALUES (7, 5, "changeme")', + ).run(); + } + + const hostRow = db + .prepare("SELECT COUNT(*) AS count FROM docker_hosts") + .get() as { count: number }; + + if (hostRow.count === 0) { + db.prepare( + "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)", + ).run("Localhost", "localhost:2375", false); + } } init(); diff --git a/src/core/database/dockerHosts.ts b/src/core/database/dockerHosts.ts index 62b9485..2c9903d 100644 --- a/src/core/database/dockerHosts.ts +++ b/src/core/database/dockerHosts.ts @@ -2,60 +2,60 @@ import { db } from "./database"; import { executeDbOperation } from "./helper"; const stmt = { - insert: db.prepare( - "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)" - ), - selectAll: db.prepare( - "SELECT id, name, hostAddress, secure FROM docker_hosts ORDER BY id DESC" - ), - update: db.prepare( - "UPDATE docker_hosts SET hostAddress = ?, secure = ?, name = ? WHERE id = ?" - ), - delete: db.prepare("DELETE FROM docker_hosts WHERE id = ?"), + insert: db.prepare( + "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)", + ), + selectAll: db.prepare( + "SELECT id, name, hostAddress, secure FROM docker_hosts ORDER BY id DESC", + ), + update: db.prepare( + "UPDATE docker_hosts SET hostAddress = ?, secure = ?, name = ? WHERE id = ?", + ), + delete: db.prepare("DELETE FROM docker_hosts WHERE id = ?"), }; export function addDockerHost(host: DockerHost) { - return executeDbOperation( - "Add Docker Host", - () => stmt.insert.run(host.name, host.hostAddress, host.secure), - () => { - if (!host.name || !host.hostAddress) - throw new Error("Missing required fields"); - if (typeof host.secure !== "boolean") - throw new TypeError("Invalid secure type"); - } - ); + return executeDbOperation( + "Add Docker Host", + () => stmt.insert.run(host.name, host.hostAddress, host.secure), + () => { + if (!host.name || !host.hostAddress) + throw new Error("Missing required fields"); + if (typeof host.secure !== "boolean") + throw new TypeError("Invalid secure type"); + }, + ); } export function getDockerHosts(): DockerHost[] { - return executeDbOperation("Get Docker Hosts", () => { - const rows = stmt.selectAll.all() as Array< - Omit & { secure: number } - >; - return rows.map((row) => ({ - ...row, - secure: row.secure === 1, - })); - }); + return executeDbOperation("Get Docker Hosts", () => { + const rows = stmt.selectAll.all() as Array< + Omit & { secure: number } + >; + return rows.map((row) => ({ + ...row, + secure: row.secure === 1, + })); + }); } 1; export function updateDockerHost(host: DockerHost) { - return executeDbOperation( - "Update Docker Host", - () => stmt.update.run(host.hostAddress, host.secure, host.name, host.id), - () => { - if (!host.id || typeof host.id !== "number") - throw new Error("Invalid host ID"); - } - ); + return executeDbOperation( + "Update Docker Host", + () => stmt.update.run(host.hostAddress, host.secure, host.name, host.id), + () => { + if (!host.id || typeof host.id !== "number") + throw new Error("Invalid host ID"); + }, + ); } export function deleteDockerHost(id: number) { - return executeDbOperation( - "Delete Docker Host", - () => stmt.delete.run(id), - () => { - if (typeof id !== "number") throw new TypeError("Invalid ID type"); - } - ); + return executeDbOperation( + "Delete Docker Host", + () => stmt.delete.run(id), + () => { + if (typeof id !== "number") throw new TypeError("Invalid ID type"); + }, + ); } diff --git a/src/core/database/index.ts b/src/core/database/index.ts index 104d75f..c381e7a 100644 --- a/src/core/database/index.ts +++ b/src/core/database/index.ts @@ -11,13 +11,13 @@ import * as logs from "~/core/database/logs"; import * as stacks from "~/core/database/stacks"; export const dbFunctions = { - ...dockerHosts, - ...logs, - ...config, - ...containerStats, - ...hostStats, - ...stacks, - ...backup, + ...dockerHosts, + ...logs, + ...config, + ...containerStats, + ...hostStats, + ...stacks, + ...backup, }; export type dbFunctions = typeof dbFunctions; diff --git a/src/core/database/stacks.ts b/src/core/database/stacks.ts index f39d01a..1c81e6d 100644 --- a/src/core/database/stacks.ts +++ b/src/core/database/stacks.ts @@ -7,20 +7,17 @@ import { executeDbOperation } from "./helper"; const stmt = { insert: db.prepare(` INSERT INTO stacks_config ( - name, version, custom, source, container_count, - stack_prefix, automatic_reboot_on_error, image_updates - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) + name, version, custom, source, compose_spec + ) VALUES (?, ?, ?, ?, ?) `), selectAll: db.prepare(` - SELECT id, name, version, custom, source, container_count, stack_prefix, - automatic_reboot_on_error, image_updates + SELECT id, name, version, custom, source, compose_spec FROM stacks_config ORDER BY name DESC `), update: db.prepare(` - UPDATE stacks_config SET - version = ?, custom = ?, source = ?, container_count = ?, - stack_prefix = ?, automatic_reboot_on_error = ?, image_updates = ? + UPDATE stacks_config + SET name = ?, custom = ?, source = ?, compose_spec = ? WHERE name = ? `), delete: db.prepare("DELETE FROM stacks_config WHERE id = ?"), @@ -33,10 +30,7 @@ export function addStack(stack: stacks_config) { stack.version, stack.custom, stack.source, - stack.container_count, - stack.stack_prefix, - stack.automatic_reboot_on_error, - stack.image_updates, + stack.compose_spec, ), ); @@ -65,11 +59,8 @@ export function updateStack(stack: stacks_config) { stack.version, stack.custom, stack.source, - stack.container_count, - stack.stack_prefix, - stack.automatic_reboot_on_error, - stack.image_updates, stack.name, + stack.compose_spec, ), ); } diff --git a/src/core/docker/store-container-stats.ts b/src/core/docker/store-container-stats.ts index 97e0bd9..33b9c0f 100644 --- a/src/core/docker/store-container-stats.ts +++ b/src/core/docker/store-container-stats.ts @@ -10,7 +10,7 @@ import { logger } from "../utils/logger"; async function storeContainerData() { try { const hosts = dbFunctions.getDockerHosts(); - logger.debug("Retrieved docker hosts for storring container data"); + logger.debug("Retrieved docker hosts for storing container data"); // Process each host concurrently and wait for them all to finish await Promise.all( diff --git a/src/core/stacks/controller.ts b/src/core/stacks/controller.ts index b6f6ddd..95a6480 100644 --- a/src/core/stacks/controller.ts +++ b/src/core/stacks/controller.ts @@ -9,339 +9,393 @@ import type { ComposeSpec, Stack } from "~/typings/docker-compose"; import { findObjectByKey } from "../utils/helpers"; const wrapProgressCallback = (progressCallback?: (log: string) => void) => { - return progressCallback - ? (chunk: Buffer, streamSource?: "stdout" | "stderr") => { - const log = chunk.toString(); - progressCallback(log); - } - : undefined; + return progressCallback + ? (chunk: Buffer, streamSource?: "stdout" | "stderr") => { + const log = chunk.toString(); + progressCallback(log); + } + : undefined; }; async function getStackName(stack_id: number): Promise { - logger.debug(`Fetching stack name for id ${stack_id}`); - const stacks = dbFunctions.getStacks(); - const stack = findObjectByKey(stacks, "id", stack_id); - if (!stack) { - throw new Error(`Stack with id ${stack_id} not found`); - } - return stack.name; + logger.debug(`Fetching stack name for id ${stack_id}`); + const stacks = dbFunctions.getStacks(); + const stack = findObjectByKey(stacks, "id", stack_id); + if (!stack) { + throw new Error(`Stack with id ${stack_id} not found`); + } + return stack.name; } async function runStackCommand( - stack_id: number, - command: ( - cwd: string, - progressCallback?: (log: string) => void, - ) => Promise, - action: string, + stack_id: number, + command: ( + cwd: string, + progressCallback?: (log: string) => void + ) => Promise, + action: string ): Promise { - try { - const stackName = await getStackName(stack_id); - const stackPath = await getStackPath({ - id: stack_id, - name: stackName, - } as Stack); - - const progressCallback = (log: string) => { - postToClient({ - type: "stack-progress", - data: { - stack_id, - action, - message: log.trim(), - timestamp: new Date().toISOString(), - }, - }); - }; - - return await command(stackPath, progressCallback); - } catch (error) { - postToClient({ - type: "stack-error", - data: { - stack_id, - action, - message: String(error), - timestamp: new Date().toISOString(), - }, - }); - throw new Error( - `Error while ${action} stack "${stack_id}": ${String(error)}`, - ); - } + try { + logger.debug( + `Starting runStackCommand for stack_id=${stack_id}, action="${action}"` + ); + + const stackName = await getStackName(stack_id); + logger.debug( + `Retrieved stack name "${stackName}" for stack_id=${stack_id}` + ); + + const stackPath = await getStackPath({ + id: stack_id, + name: stackName, + } as Stack); + logger.debug(`Resolved stack path "${stackPath}" for stack_id=${stack_id}`); + + const progressCallback = (log: string) => { + const message = log.trim(); + logger.debug( + `Progress for stack_id=${stack_id}, action="${action}": ${message}` + ); + + // ERROR HANDLING FOR COMPOSE ACTIONS + if (message.includes("Error response from daemon")) { + logger.error( + `Error response from daemon: ${ + message.split("Error response from daemon:")[1] + }` + ); + } + + postToClient({ + type: "stack-progress", + data: { + stack_id, + action, + message, + timestamp: new Date().toISOString(), + }, + }); + }; + + logger.debug( + `Executing command for stack_id=${stack_id}, action="${action}"` + ); + const result = await command(stackPath, progressCallback); + logger.debug( + `Successfully completed command for stack_id=${stack_id}, action="${action}"` + ); + + return result; + } catch (error) { + logger.debug( + `Error occurred for stack_id=${stack_id}, action="${action}": ${String( + error + )}` + ); + postToClient({ + type: "stack-error", + data: { + stack_id, + action, + message: String(error), + timestamp: new Date().toISOString(), + }, + }); + throw new Error( + `Error while ${action} stack "${stack_id}": ${String(error)}` + ); + } } async function getStackPath(stack: Stack): Promise { - const stackName = stack.name.trim().replace(/\s+/g, "_"); - return `stacks/${stackName}`; + const stackName = stack.name.trim().replace(/\s+/g, "_"); + const stackId = stack.id; + + if (!stackId) { + logger.error("Stack could not be parsed"); + throw new Error("Stack could not be parsed"); + } + + return `stacks/${stackId}-${stackName}`; } async function createStackYAML(compose_spec: Stack): Promise { - const yaml = YAML.stringify(compose_spec.compose_spec); - const stackPath = await getStackPath(compose_spec); - await Bun.write(`${stackPath}/docker-compose.yaml`, yaml, { - createPath: true, - }); + const yaml = YAML.stringify(compose_spec.compose_spec); + const stackPath = await getStackPath(compose_spec); + await Bun.write(`${stackPath}/docker-compose.yaml`, yaml, { + createPath: true, + }); } -export async function deployStack( - stack: ComposeSpec, - name: string, - version: number, - source: string, - automatic_reboot_on_error: boolean, - isCustom: boolean, - image_updates: boolean, - stack_prefix?: string, -): Promise { - let stackId: number; - - try { - logger.debug(`Deploying Stack: ${JSON.stringify(stack)}`); - const serviceCount = stack.services - ? Object.keys(stack.services).length - : 0; - const resolvedPrefix = stack_prefix ?? ""; - - const stack_config: stacks_config = { - id: 0, - name, - version, - source, - stack_prefix: resolvedPrefix, - automatic_reboot_on_error, - container_count: serviceCount, - custom: isCustom, - image_updates, - }; - - if (!name) { - throw new Error("Stack name needed"); - } - - stackId = dbFunctions.addStack(stack_config) as number; - postToClient({ - type: "stack-status", - data: { - stack_id: stackId, - status: "pending", - message: "Creating stack configuration", - }, - }); - - const stackYaml: Stack = { - id: stackId, - name, - source, - version, - compose_spec: stack, - }; - - await createStackYAML(stackYaml); - - await runStackCommand( - stackId, - (cwd, progressCallback) => - DockerCompose.upAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "deploying", - ); - - postToClient({ - type: "stack-status", - data: { - stack_id: stackId, - status: "deployed", - message: "Stack deployed successfully", - }, - }); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - postToClient({ - type: "stack-error", - data: { - stack_id: 0, - action: "deploying", - message: errorMsg, - timestamp: new Date().toISOString(), - }, - }); - throw new Error(errorMsg); - } +export async function deployStack(stack_config: stacks_config): Promise { + try { + logger.debug(`Deploying Stack: ${JSON.stringify(stack_config)}`); + + if (!stack_config.name) { + throw new Error("Stack name needed"); + } + + const jsonStringStack = { + ...stack_config, + compose_spec: JSON.stringify(stack_config.compose_spec), + }; + + const stackId = dbFunctions.addStack(jsonStringStack); + + if (!stackId) { + throw new Error("Failed to add stack to database"); + } + + postToClient({ + type: "stack-status", + data: { + stack_id: stackId, + status: "pending", + message: "Creating stack configuration", + }, + }); + + const stackYaml: Stack = { + id: stackId, + name: stack_config.name, + source: stack_config.source, + version: stack_config.version, + compose_spec: stack_config.compose_spec as unknown as ComposeSpec, // Weird stuff i am doing here... smh + }; + + await createStackYAML(stackYaml); + + await runStackCommand( + stackId, + (cwd, progressCallback) => + DockerCompose.upAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "deploying" + ); + + postToClient({ + type: "stack-status", + data: { + stack_id: stackId, + status: "deployed", + message: "Stack deployed successfully", + }, + }); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + postToClient({ + type: "stack-error", + data: { + stack_id: 0, + action: "deploying", + message: errorMsg, + timestamp: new Date().toISOString(), + }, + }); + throw new Error(errorMsg); + } } export async function stopStack(stack_id: number): Promise { - // Note the await to discard the result (convert to void) - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.downAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "stopping", - ); + // Note the await to discard the result (convert to void) + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.downAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "stopping" + ); } export async function startStack(stack_id: number): Promise { - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.upAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "starting", - ); + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.upAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "starting" + ); } export async function pullStackImages(stack_id: number): Promise { - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.pullAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "pulling-images", - ); + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.pullAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "pulling-images" + ); } export async function restartStack(stack_id: number): Promise { - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.restartAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "restarting", - ); + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.restartAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "restarting" + ); } export async function getStackStatus( - stack_id: number, - //biome-ignore lint/suspicious/noExplicitAny: + stack_id: number + //biome-ignore lint/suspicious/noExplicitAny: ): Promise> { - const status = await runStackCommand( - stack_id, - async (cwd) => { - const rawStatus = await DockerCompose.ps({ cwd }); - //biome-ignore lint/suspicious/noExplicitAny: - return rawStatus.data.services.reduce((acc: any, service: any) => { - acc[service.name] = service.state; - return acc; - }, {}); - }, - "status-check", - ); - return status; + const status = await runStackCommand( + stack_id, + async (cwd) => { + const rawStatus = await DockerCompose.ps({ cwd }); + //biome-ignore lint/suspicious/noExplicitAny: + return rawStatus.data.services.reduce((acc: any, service: any) => { + acc[service.name] = service.state; + return acc; + }, {}); + }, + "status-check" + ); + return status; } export async function removeStack(stack_id: number): Promise { - try { - await runStackCommand( - stack_id, - async (cwd, progressCallback) => { - await DockerCompose.down({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }); - }, - "removing", - ); - - const stackName = await getStackName(stack_id); - const stackPath = await getStackPath({ - id: stack_id, - name: stackName, - } as Stack); - - try { - await rm(stackPath, { recursive: true }); - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - postToClient({ - type: "stack-error", - data: { - stack_id, - action: "removing", - message: errorMsg, - timestamp: new Date().toISOString(), - }, - }); - throw new Error(errorMsg); - } - - dbFunctions.deleteStack(stack_id); - postToClient({ - type: "stack-removed", - data: { - stack_id, - message: "Stack removed successfully", - }, - }); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - postToClient({ - type: "stack-error", - data: { - stack_id, - action: "removing", - message: errorMsg, - timestamp: new Date().toISOString(), - }, - }); - throw new Error(errorMsg); - } + try { + const _ = dbFunctions.deleteStack(stack_id); + + await runStackCommand( + stack_id, + async (cwd, progressCallback) => { + await DockerCompose.down({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }); + }, + "removing" + ); + + const stackName = await getStackName(stack_id); + const stackPath = await getStackPath({ + id: stack_id, + name: stackName, + } as Stack); + + try { + await rm(stackPath, { recursive: true }); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + postToClient({ + type: "stack-error", + data: { + stack_id, + action: "removing", + message: errorMsg, + timestamp: new Date().toISOString(), + }, + }); + throw new Error(errorMsg); + } + + postToClient({ + type: "stack-removed", + data: { + stack_id, + message: "Stack removed successfully", + }, + }); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + postToClient({ + type: "stack-error", + data: { + stack_id, + action: "removing", + message: errorMsg, + timestamp: new Date().toISOString(), + }, + }); + throw new Error(errorMsg); + } } -//biome-ignore lint/suspicious/noExplicitAny: -export async function getAllStacksStatus(): Promise> { - try { - const stacks = dbFunctions.getStacks(); - - const statusResults = await Promise.all( - stacks.map(async (stack) => { - const status = await runStackCommand( - stack.id as number, - async (cwd) => { - const rawStatus = await DockerCompose.ps({ cwd }); - //biome-ignore lint/suspicious/noExplicitAny: - return rawStatus.data.services.reduce((acc: any, service: any) => { - acc[service.name] = service.state; - return acc; - }, {}); - }, - "status-check", - ); - return { stackId: stack.id, status }; - }), - ); - - return statusResults.reduce( - (acc, { stackId, status }) => { - // Ensure stackId is used as a string if necessary, e.g. - acc[String(stackId)] = status; - return acc; - }, - //biome-ignore lint/suspicious/noExplicitAny: - {} as Record, - ); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - throw new Error(errorMsg); - } +interface DockerServiceStatus { + status: string; + ports: string[]; +} + +interface StackStatus { + services: Record; + healthy: number; + unhealthy: number; + total: number; +} + +type StacksStatus = Record; + +export async function getAllStacksStatus(): Promise { + try { + const stacks = dbFunctions.getStacks(); + + const statusResults = await Promise.all( + stacks.map(async (stack) => { + const status = await runStackCommand( + stack.id as number, + async (cwd) => { + const rawStatus = await DockerCompose.ps({ cwd }); + const services = rawStatus.data.services.reduce( + (acc: Record, service) => { + acc[service.name] = { + status: service.state, + ports: service.ports.map( + (port) => `${port.mapped?.address}:${port.mapped?.port}` + ), + }; + return acc; + }, + {} + ); + + const statusValues = Object.values(services); + return { + services, + healthy: statusValues.filter( + (s) => s.status === "running" || s.status.includes("Up") + ).length, + unhealthy: statusValues.filter( + (s) => s.status !== "running" && !s.status.includes("Up") + ).length, + total: statusValues.length, + }; + }, + "status-check" + ); + return { stackId: stack.id, status }; + }) + ); + + return statusResults.reduce((acc, { stackId, status }) => { + acc[String(stackId)] = status; + return acc; + }, {} as StacksStatus); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + throw new Error(errorMsg); + } } diff --git a/src/core/utils/helpers.ts b/src/core/utils/helpers.ts index ab13dd4..6c3e79e 100644 --- a/src/core/utils/helpers.ts +++ b/src/core/utils/helpers.ts @@ -5,7 +5,9 @@ export function findObjectByKey( key: keyof T, value: T[keyof T], ): T | undefined { - logger.debug(`Searching ${String(key)}`); + logger.debug( + `Searching for key: ${String(key)} with value: ${String(value)}`, + ); const data = array.find((item) => item[key] === value); return data; } diff --git a/src/index.ts b/src/index.ts index 419c6bf..e52e8a7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ import staticPlugin from "@elysiajs/static"; import { swagger } from "@elysiajs/swagger"; import { Elysia } from "elysia"; import { dts } from "elysia-remote-dts"; +import { Logestic } from "logestic"; import { dbFunctions } from "~/core/database"; import { monitorDockerEvents } from "~/core/docker/monitor"; import { setSchedules } from "~/core/docker/scheduler"; @@ -22,7 +23,6 @@ import { dockerWebsocketRoutes } from "~/routes/docker-websocket"; import { liveLogs } from "~/routes/live-logs"; import { backendLogs } from "~/routes/logs"; import { stackRoutes } from "~/routes/stacks"; -import { utilRoutes } from "~/routes/utils"; import type { config } from "~/typings/database"; import { liveStacks } from "./routes/live-stacks"; @@ -30,7 +30,11 @@ console.log(""); logger.info("Starting DockStatAPI"); -const DockStatAPI = new Elysia() +const DockStatAPI = new Elysia({ + normalize: true, + precompile: true, +}) + .use(Logestic.preset("fancy")) .use(staticPlugin()) .use(serverTiming()) .use( @@ -92,7 +96,7 @@ const DockStatAPI = new Elysia() if ( path === "/health" || path.startsWith("/swagger") || - path.startsWith("/trpc") + path.startsWith("/public") ) { logger.info(`Requested unguarded route: ${path}`); return; @@ -100,26 +104,34 @@ const DockStatAPI = new Elysia() const validation = await validateApiKey(request, set); - if (validation.error) { + if (!validation) { + throw new Error("Error while checking API key"); + } + + if (!validation.success) { set.status = 400; - return { error: validation.error }; + throw new Error(validation.error); } }) - .onError(({ code, set, path }) => { + .onError(({ code, set, path, error }) => { if (code === "NOT_FOUND") { logger.warn(`Unknown route (${path}), showing error page!`); set.status = 404; set.headers["Content-Type"] = "text/html"; return Bun.file("public/404.html"); } + + logger.error(`Internal server error at ${path}: ${error.message}`); + set.status = 500; + set.headers["Content-Type"] = "text/html"; + return { success: false, message: error.message }; }) .use(dockerRoutes) .use(dockerStatsRoutes) .use(backendLogs) .use(dockerWebsocketRoutes) .use(apiConfigRoutes) - .use(utilRoutes) .use(stackRoutes) .use(liveLogs) .use(liveStacks) diff --git a/src/middleware/auth.ts b/src/middleware/auth.ts index 0007793..3a73022 100644 --- a/src/middleware/auth.ts +++ b/src/middleware/auth.ts @@ -47,13 +47,13 @@ export async function validateApiKey(request: Request, set: set) { logger.warn( "API Key validation deactivated, since running in development mode", ); - return { apiKey }; + return { success: true, apiKey }; } if (!apiKey) { logger.error(`API key missing from request ${request.url}`); set.status = 401; - return { error: "API key required" }; + return { error: "API key required", success: false, apiKey }; } logger.debug("API key validation initiated"); @@ -64,7 +64,12 @@ export async function validateApiKey(request: Request, set: set) { if (!dbRecord) { logger.error("API key not found in database"); set.status = 401; - return { error: "Invalid API key" }; + return { success: false, error: "Invalid API key" }; + } + + if (dbRecord.hash === "changeme") { + logger.error("Please change your API Key!"); + return { success: true, apiKey }; } const isValid = await validateApiKeyHash(apiKey, dbRecord.hash); @@ -72,13 +77,13 @@ export async function validateApiKey(request: Request, set: set) { if (!isValid) { logger.error("Invalid API key provided"); set.status = 401; - return { error: "Invalid API key" }; + return { success: false, error: "Invalid API key", apiKey }; } - return logger.info("Valid API key used"); + logger.info("Valid API key used"); } catch (error) { logger.error("Error during API key validation", error); set.status = 500; - return { error: "Internal server error" }; + return { success: false, error: "Internal server error", apiKey }; } } diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index 7e51d8b..8759505 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -32,11 +32,8 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) logger.debug("Fetched backend config"); return distinct; } catch (error) { - return responseHandler.error( - set, - error as string, - "Error getting the DockStatAPI config", - ); + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); } }, { @@ -95,11 +92,8 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) try { return pluginManager.getLoadedPlugins(); } catch (error) { - return responseHandler.error( - set, - error as string, - "Error getting all registered plugins", - ); + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); } }, { @@ -168,11 +162,8 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) ); return responseHandler.ok(set, "Updated DockStatAPI config"); } catch (error) { - return responseHandler.error( - set, - "Error updating the DockStatAPI config", - error as string, - ); + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); } }, { @@ -224,10 +215,10 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) ) .get( "/package", - async ({ set }) => { + async () => { try { logger.debug("Fetching package.json"); - return { + const data = { version: version, description: description, license: license, @@ -238,12 +229,19 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) dependencies: dependencies, devDependencies: devDependencies, }; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Error while reading package.json", + + logger.debug( + `Received: ${JSON.stringify(data).length} chars in package.json`, ); + + if (JSON.stringify(data).length <= 10) { + throw new Error("Failed to read package.json"); + } + + return data; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); } }, { @@ -337,7 +335,8 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) const backupFilename = await dbFunctions.backupDatabase(); return responseHandler.ok(set, backupFilename); } catch (error) { - return responseHandler.error(set, error as string, "Error backing up"); + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); } }, { @@ -397,11 +396,8 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) return filteredFiles; } catch (error) { - return responseHandler.error( - set, - error as string, - "Reading Backup directory", - ); + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); } }, { @@ -463,11 +459,8 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) `attachment; filename="${filename}"`; return Bun.file(filePath); } catch (error) { - return responseHandler.error( - set, - error as string, - "Backup download failed", - ); + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); } }, { @@ -545,11 +538,8 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) return responseHandler.ok(set, "Database restored successfully"); } catch (error) { - return responseHandler.error( - set, - error instanceof Error ? error.message : "Restoration failed", - "Database restoration error", - ); + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); } }, { diff --git a/src/routes/live-stacks.ts b/src/routes/live-stacks.ts index b3a14e7..2b5e8e3 100644 --- a/src/routes/live-stacks.ts +++ b/src/routes/live-stacks.ts @@ -1,6 +1,5 @@ import { Elysia } from "elysia"; import type { ElysiaWS } from "elysia/dist/ws"; - import { logger } from "~/core/utils/logger"; import type { stackSocketMessage } from "~/typings/websocket"; @@ -8,24 +7,24 @@ import type { stackSocketMessage } from "~/typings/websocket"; const activeConnections = new Set>(); export const liveStacks = new Elysia().ws("/stacks", { - open(ws) { - activeConnections.add(ws); - ws.send({ message: "Connection established" }); - logger.info(`New Stacks WebSocket established (${ws.id})`); - }, - close(ws) { - logger.info(`Stacks WebSocket closed (${ws.id})`); - activeConnections.delete(ws); - }, + open(ws) { + activeConnections.add(ws); + ws.send({ message: "Connection established" }); + logger.info(`New Stacks WebSocket established (${ws.id})`); + }, + close(ws) { + logger.info(`Stacks WebSocket closed (${ws.id})`); + activeConnections.delete(ws); + }, }); export function postToClient(data: stackSocketMessage) { - for (const ws of activeConnections) { - try { - ws.send(JSON.stringify(data)); - } catch (error) { - activeConnections.delete(ws); - logger.error("Failed to send to WebSocket:", error); - } - } + for (const ws of activeConnections) { + try { + ws.send(JSON.stringify(data)); + } catch (error) { + activeConnections.delete(ws); + logger.error("Failed to send to WebSocket:", error); + } + } } diff --git a/src/routes/stacks.ts b/src/routes/stacks.ts index d5b2242..3ac2b86 100644 --- a/src/routes/stacks.ts +++ b/src/routes/stacks.ts @@ -1,5 +1,4 @@ import { Elysia, t } from "elysia"; - import { dbFunctions } from "~/core/database"; import { deployStack, @@ -13,45 +12,14 @@ import { } from "~/core/stacks/controller"; import { logger } from "~/core/utils/logger"; import { responseHandler } from "~/core/utils/response-handler"; +import type { stacks_config } from "~/typings/database"; export const stackRoutes = new Elysia({ prefix: "/stacks" }) .post( "/deploy", async ({ set, body }) => { try { - const isCustom = body.isCustom || false; - - const image_updates = body.image_updates || false; - - const missingParams: string[] = []; - if (!body.compose_spec) { - missingParams.push("compose_spec"); - } - if (body.automatic_reboot_on_error === undefined) { - missingParams.push("automatic_reboot_on_error"); - } - if (!body.source) { - missingParams.push("source"); - } - if (!body.name) { - missingParams.push("name"); - } - - if (missingParams.length > 0) { - const errMsg = `Missing values of: ${missingParams.join("; ")}`; - return responseHandler.error(set, errMsg, errMsg); - } - - await deployStack( - body.compose_spec, - body.name, - body.version, - body.source, - body.automatic_reboot_on_error, - isCustom, - image_updates, - body.stack_prefix, - ); + await deployStack(body as stacks_config); logger.info(`Deployed Stack (${body.name})`); return responseHandler.ok( set, @@ -60,7 +28,11 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); - return responseHandler.error(set, errorMsg, "Error deploying stack"); + return responseHandler.error( + set, + errorMsg, + "Error deploying stack, please check the server logs for more information", + ); } }, { @@ -104,14 +76,11 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) }, }, body: t.Object({ - compose_spec: t.Any(), name: t.String(), version: t.Number(), - automatic_reboot_on_error: t.Boolean(), - isCustom: t.Boolean(), - image_updates: t.Boolean(), + custom: t.Boolean(), source: t.String(), - stack_prefix: t.Optional(t.String()), + compose_spec: t.Any(), }), }, ) @@ -375,24 +344,41 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) "/status", async ({ set, query }) => { try { - //biome-ignore lint/suspicious/noExplicitAny: + // biome-ignore lint/suspicious/noExplicitAny: let status: Record; let res = {}; + + logger.debug("Entering stack status handler"); + logger.debug(`Request body: ${JSON.stringify(query)}`); + if (query.stackId) { + logger.debug(`Fetching status for stackId=${query.stackId}`); status = await getStackStatus(query.stackId); + logger.debug( + `Retrieved status for stackId=${query.stackId}: ${JSON.stringify(status)}`, + ); + res = responseHandler.ok( set, `Stack ${query.stackId} status retrieved successfully`, ); logger.info("Fetched Stack status"); } else { + logger.debug("Fetching status for all stacks"); status = await getAllStacksStatus(); + logger.debug( + `Retrieved status for all stacks: ${JSON.stringify(status)}`, + ); + res = responseHandler.ok(set, "Fetched all Stack's status"); logger.info("Fetched all Stack status"); } + + logger.debug("Returning response with status data"); return { ...res, status: status }; } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); + logger.debug(`Error occurred while fetching stack status: ${errorMsg}`); return responseHandler.error( set, @@ -470,9 +456,11 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) }, }, }, - query: t.Object({ - stackId: t.Number(), - }), + query: t.Optional( + t.Object({ + stackId: t.Number(), + }), + ), }, ) .get( @@ -549,7 +537,6 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) }, }, ) - .delete( "/", async ({ set, body }) => { diff --git a/src/routes/utils.ts b/src/routes/utils.ts index 591efd5..e69de29 100644 --- a/src/routes/utils.ts +++ b/src/routes/utils.ts @@ -1,123 +0,0 @@ -import { Elysia, t } from "elysia"; - -import { - authorEmail, - authorName, - authorWebsite, - contributors, - dependencies, - description, - devDependencies, - license, - version, -} from "~/core/utils/package-json"; -import { responseHandler } from "~/core/utils/response-handler"; - -export const utilRoutes = new Elysia({ prefix: "/utils" }).get( - "/info", - async ({ set }) => { - try { - set.status = 200; - return { - version, - authorEmail, - authorName, - authorWebsite, - contributors, - dependencies, - description, - devDependencies, - license, - }; - } catch (error) { - return responseHandler.error( - set, - String(error), - "Error getting DockStatAPI information", - ); - } - }, - { - detail: { - tags: ["Utils"], - description: - "Retrieves DockStatAPI metadata including version, author information, dependencies, and licensing details", - responses: { - "200": { - description: "Successfully retrieved API information", - content: { - "application/json": { - schema: { - type: "object", - properties: { - version: { - type: "string", - example: "3.0.0", - }, - authorEmail: { - type: "string", - example: "info@itsnik.de", - }, - authorName: { - type: "string", - example: "ItsNik", - }, - authorWebsite: { - type: "string", - example: "https://github.com/Its4Nik", - }, - contributors: { - type: "array", - items: { - type: "string", - }, - example: [], - }, - dependencies: { - type: "object", - example: { - "@elysiajs/server-timing": "^1.2.1", - "@elysiajs/static": "^1.2.0", - }, - }, - description: { - type: "string", - example: - "DockStatAPI is an API backend featuring plugins and more for DockStat", - }, - devDependencies: { - type: "object", - example: { - "@biomejs/biome": "1.9.4", - "@types/dockerode": "^3.3.38", - }, - }, - license: { - type: "string", - example: "CC BY-NC 4.0", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving API information", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error getting DockStatAPI information", - }, - }, - }, - }, - }, - }, - }, - }, - }, -); diff --git a/src/tests/api-config.spec.ts b/src/tests/api-config.spec.ts index b97d8d4..d1f9d09 100644 --- a/src/tests/api-config.spec.ts +++ b/src/tests/api-config.spec.ts @@ -6,339 +6,339 @@ import { generateJunitReport, recordTestResult } from "./junit-exporter"; import type { TestContext } from "./junit-exporter"; const mockDb = { - updateConfig: mock(() => ({})), - backupDatabase: mock( - () => `dockstatapi-${new Date().toISOString().slice(0, 10)}.db.bak` - ), - restoreDatabase: mock(), - findLatestBackup: mock(() => "dockstatapi-2025-05-06.db.bak"), + updateConfig: mock(() => ({})), + backupDatabase: mock( + () => `dockstatapi-${new Date().toISOString().slice(0, 10)}.db.bak`, + ), + restoreDatabase: mock(), + findLatestBackup: mock(() => "dockstatapi-2025-05-06.db.bak"), }; mock.module("node:fs", () => ({ - existsSync: mock((path) => path.includes("dockstatapi")), - readdirSync: mock(() => [ - "dockstatapi-2025-05-06.db.bak", - "dockstatapi.db", - "dockstatapi.db-shm", - ]), - unlinkSync: mock(), + existsSync: mock((path) => path.includes("dockstatapi")), + readdirSync: mock(() => [ + "dockstatapi-2025-05-06.db.bak", + "dockstatapi.db", + "dockstatapi.db-shm", + ]), + unlinkSync: mock(), })); const mockPlugins = [ - { - name: "docker-monitor", - version: "1.2.0", - status: "active", - }, + { + name: "docker-monitor", + version: "1.2.0", + status: "active", + }, ]; const createTestApp = () => - new Elysia().use(apiConfigRoutes).decorate({ - dbFunctions: mockDb, - pluginManager: { - getLoadedPlugins: mock(() => mockPlugins), - getPlugin: mock((name) => mockPlugins.find((p) => p.name === name)), - }, - logger: { - ...logger, - debug: mock(), - error: mock(), - info: mock(), - }, - }); + new Elysia().use(apiConfigRoutes).decorate({ + dbFunctions: mockDb, + pluginManager: { + getLoadedPlugins: mock(() => mockPlugins), + getPlugin: mock((name) => mockPlugins.find((p) => p.name === name)), + }, + logger: { + ...logger, + debug: mock(), + error: mock(), + info: mock(), + }, + }); async function captureTestContext( - req: Request, - res: Response + req: Request, + res: Response, ): Promise { - const responseStatus = res.status; - const responseHeaders = Object.fromEntries(res.headers.entries()); - let responseBody: string; - - try { - responseBody = await res.clone().json(); - } catch (parseError) { - try { - responseBody = await res.clone().text(); - } catch (textError) { - responseBody = "Unparseable response content"; - } - } - - return { - request: { - method: req.method, - url: req.url, - headers: Object.fromEntries(req.headers.entries()), - body: req.body ? await req.clone().text() : undefined, - }, - response: { - status: responseStatus, - headers: responseHeaders, - body: responseBody, - }, - }; + const responseStatus = res.status; + const responseHeaders = Object.fromEntries(res.headers.entries()); + let responseBody: string; + + try { + responseBody = await res.clone().json(); + } catch (parseError) { + try { + responseBody = await res.clone().text(); + } catch (textError) { + responseBody = "Unparseable response content"; + } + } + + return { + request: { + method: req.method, + url: req.url, + headers: Object.fromEntries(req.headers.entries()), + body: req.body ? await req.clone().text() : undefined, + }, + response: { + status: responseStatus, + headers: responseHeaders, + body: responseBody, + }, + }; } describe("API Configuration Endpoints", () => { - beforeEach(() => { - mockDb.updateConfig.mockClear(); - }); - - describe("Core Configuration", () => { - it("should retrieve current config with hashed API key", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - try { - const app = createTestApp(); - const req = new Request("http://localhost:3000/config"); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - fetching_interval: expect.any(Number), - keep_data_for: expect.any(Number), - }); - - recordTestResult({ - name: "should retrieve current config with hashed API key", - suite: "API Configuration Endpoints - Core Configuration", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "should retrieve current config with hashed API key", - suite: "API Configuration Endpoints - Core Configuration", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with valid config structure", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle config update with valid payload", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - try { - const app = createTestApp(); - const requestBody = { - fetching_interval: 15, - keep_data_for: 30, - api_key: "new-valid-key", - }; - const req = new Request("http://localhost:3000/config/update", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(requestBody), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - success: true, - message: expect.stringContaining("Updated"), - }); - - recordTestResult({ - name: "should handle config update with valid payload", - suite: "API Configuration Endpoints - Core Configuration", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "should handle config update with valid payload", - suite: "API Configuration Endpoints - Core Configuration", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with update confirmation", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("Plugin Management", () => { - it("should list active plugins with metadata", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - try { - const app = createTestApp(); - const req = new Request("http://localhost:3000/config/plugins"); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toEqual( - [] - //expect.arrayContaining([ - // expect.objectContaining({ - // name: expect.any(String), - // version: expect.any(String), - // status: expect.any(String), - // }), - //]) - ); - - recordTestResult({ - name: "should list active plugins with metadata", - suite: "API Configuration Endpoints - Plugin Management", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "should list active plugins with metadata", - suite: "API Configuration Endpoints - Plugin Management", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with plugin list", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("Backup Management", () => { - it("should generate timestamped backup files", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - try { - const app = createTestApp(); - const req = new Request("http://localhost:3000/config/backup", { - method: "POST", - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - const { message } = context.response.body as { message: string }; - expect(message).toMatch( - /^data\/dockstatapi-\d{2}-\d{2}-\d{4}-1\.db\.bak$/ - ); - - recordTestResult({ - name: "should generate timestamped backup files", - suite: "API Configuration Endpoints - Backup Management", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "should generate timestamped backup files", - suite: "API Configuration Endpoints - Backup Management", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with backup path", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should list valid backup files", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - try { - const app = createTestApp(); - const req = new Request("http://localhost:3000/config/backup"); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - const backups = context.response.body as string[]; - expect(backups).toEqual( - expect.arrayContaining([expect.stringMatching(/\.db\.bak$/)]) - ); - - recordTestResult({ - name: "should list valid backup files", - suite: "API Configuration Endpoints - Backup Management", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "should list valid backup files", - suite: "API Configuration Endpoints - Backup Management", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with backup list", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("Error Handling", () => { - it("should return proper error format", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - try { - const app = createTestApp(); - const req = new Request("http://localhost:3000/random_link", { - method: "GET", - headers: { "Content-Type": "application/json" }, - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(404); - - recordTestResult({ - name: "should return proper error format", - suite: - "API Configuration Endpoints - Error Handling of unkown routes", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "should return proper error format", - suite: "API Configuration Endpoints - Error Handling", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "500 Error with structured error format", - received: context?.response, - }, - }); - throw error; - } - }); - }); + beforeEach(() => { + mockDb.updateConfig.mockClear(); + }); + + describe("Core Configuration", () => { + it("should retrieve current config with hashed API key", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const req = new Request("http://localhost:3000/config"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + fetching_interval: expect.any(Number), + keep_data_for: expect.any(Number), + }); + + recordTestResult({ + name: "should retrieve current config with hashed API key", + suite: "API Configuration Endpoints - Core Configuration", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should retrieve current config with hashed API key", + suite: "API Configuration Endpoints - Core Configuration", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with valid config structure", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle config update with valid payload", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const requestBody = { + fetching_interval: 15, + keep_data_for: 30, + api_key: "new-valid-key", + }; + const req = new Request("http://localhost:3000/config/update", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(requestBody), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + success: true, + message: expect.stringContaining("Updated"), + }); + + recordTestResult({ + name: "should handle config update with valid payload", + suite: "API Configuration Endpoints - Core Configuration", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should handle config update with valid payload", + suite: "API Configuration Endpoints - Core Configuration", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with update confirmation", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("Plugin Management", () => { + it("should list active plugins with metadata", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const req = new Request("http://localhost:3000/config/plugins"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toEqual( + [], + //expect.arrayContaining([ + // expect.objectContaining({ + // name: expect.any(String), + // version: expect.any(String), + // status: expect.any(String), + // }), + //]) + ); + + recordTestResult({ + name: "should list active plugins with metadata", + suite: "API Configuration Endpoints - Plugin Management", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should list active plugins with metadata", + suite: "API Configuration Endpoints - Plugin Management", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with plugin list", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("Backup Management", () => { + it("should generate timestamped backup files", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const req = new Request("http://localhost:3000/config/backup", { + method: "POST", + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + const { message } = context.response.body as { message: string }; + expect(message).toMatch( + /^data\/dockstatapi-\d{2}-\d{2}-\d{4}-1\.db\.bak$/, + ); + + recordTestResult({ + name: "should generate timestamped backup files", + suite: "API Configuration Endpoints - Backup Management", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should generate timestamped backup files", + suite: "API Configuration Endpoints - Backup Management", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with backup path", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should list valid backup files", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const req = new Request("http://localhost:3000/config/backup"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + const backups = context.response.body as string[]; + expect(backups).toEqual( + expect.arrayContaining([expect.stringMatching(/\.db\.bak$/)]), + ); + + recordTestResult({ + name: "should list valid backup files", + suite: "API Configuration Endpoints - Backup Management", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should list valid backup files", + suite: "API Configuration Endpoints - Backup Management", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with backup list", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("Error Handling", () => { + it("should return proper error format", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const req = new Request("http://localhost:3000/random_link", { + method: "GET", + headers: { "Content-Type": "application/json" }, + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(404); + + recordTestResult({ + name: "should return proper error format", + suite: + "API Configuration Endpoints - Error Handling of unkown routes", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should return proper error format", + suite: "API Configuration Endpoints - Error Handling", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "500 Error with structured error format", + received: context?.response, + }, + }); + throw error; + } + }); + }); }); afterAll(() => { - generateJunitReport(); + generateJunitReport(); }); diff --git a/src/tests/docker-manager.spec.ts b/src/tests/docker-manager.spec.ts index b8864e8..962937a 100644 --- a/src/tests/docker-manager.spec.ts +++ b/src/tests/docker-manager.spec.ts @@ -3,462 +3,462 @@ import { Elysia } from "elysia"; import { dbFunctions } from "~/core/database"; import { dockerRoutes } from "~/routes/docker-manager"; import { - generateJunitReport, - recordTestResult, - testResults, + generateJunitReport, + recordTestResult, + testResults, } from "./junit-exporter"; import type { TestContext } from "./junit-exporter"; type DockerHost = { - id?: number; - name: string; - hostAddress: string; - secure: boolean; + id?: number; + name: string; + hostAddress: string; + secure: boolean; }; const mockDb = { - addDockerHost: mock(() => ({ - changes: 1, - lastInsertRowid: 1, - })), - updateDockerHost: mock(() => ({ - changes: 1, - lastInsertRowid: 1, - })), - getDockerHosts: mock(() => []), - deleteDockerHost: mock(() => ({ - changes: 1, - lastInsertRowid: 1, - })), + addDockerHost: mock(() => ({ + changes: 1, + lastInsertRowid: 1, + })), + updateDockerHost: mock(() => ({ + changes: 1, + lastInsertRowid: 1, + })), + getDockerHosts: mock(() => []), + deleteDockerHost: mock(() => ({ + changes: 1, + lastInsertRowid: 1, + })), }; mock.module("~/core/database", () => ({ - dbFunctions: mockDb, + dbFunctions: mockDb, })); mock.module("~/core/utils/logger", () => ({ - logger: { - debug: mock(), - info: mock(), - error: mock(), - }, + logger: { + debug: mock(), + info: mock(), + error: mock(), + }, })); const createApp = () => new Elysia().use(dockerRoutes).decorate({}); async function captureTestContext( - req: Request, - res: Response + req: Request, + res: Response, ): Promise { - const responseStatus = res.status; - const responseHeaders = Object.fromEntries(res.headers.entries()); - let responseBody: unknown; - - try { - responseBody = await res.clone().json(); - } catch (parseError) { - try { - responseBody = await res.clone().text(); - } catch { - responseBody = "Unparseable response content"; - } - } - - return { - request: { - method: req.method, - url: req.url, - headers: Object.fromEntries(req.headers.entries()), - body: req.body ? await req.clone().text() : undefined, - }, - response: { - status: responseStatus, - headers: responseHeaders, - body: responseBody, - }, - }; + const responseStatus = res.status; + const responseHeaders = Object.fromEntries(res.headers.entries()); + let responseBody: unknown; + + try { + responseBody = await res.clone().json(); + } catch (parseError) { + try { + responseBody = await res.clone().text(); + } catch { + responseBody = "Unparseable response content"; + } + } + + return { + request: { + method: req.method, + url: req.url, + headers: Object.fromEntries(req.headers.entries()), + body: req.body ? await req.clone().text() : undefined, + }, + response: { + status: responseStatus, + headers: responseHeaders, + body: responseBody, + }, + }; } describe("Docker Configuration Endpoints", () => { - beforeEach(() => { - mockDb.addDockerHost.mockClear(); - mockDb.updateDockerHost.mockClear(); - mockDb.getDockerHosts.mockClear(); - mockDb.deleteDockerHost.mockClear(); - }); - - describe("POST /docker-config/add-host", () => { - it("should add a docker host successfully", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const host: DockerHost = { - name: "Host1", - hostAddress: "127.0.0.1:2375", - secure: false, - }; - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/add-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - message: `Added docker host (${host.name})`, - }); - expect(mockDb.addDockerHost).toHaveBeenCalledWith(host); - - recordTestResult({ - name: "add-host success", - suite: "Docker Config - Add Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "add-host success", - suite: "Docker Config - Add Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with success message", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle error when adding a docker host fails", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const host: DockerHost = { - name: "Host2", - hostAddress: "invalid", - secure: true, - }; - - // Set mock implementation - mockDb.addDockerHost.mockImplementationOnce(() => { - throw new Error("DB error"); - }); - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/add-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - error: expect.any(String), - }); - - recordTestResult({ - name: "add-host failure", - suite: "Docker Config - Add Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "add-host failure", - suite: "Docker Config - Add Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "400 Error with error structure", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("POST /docker-config/update-host", () => { - it("should update a docker host successfully", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const host: DockerHost = { - id: 1, - name: "Host1-upd", - hostAddress: "127.0.0.1:2376", - secure: true, - }; - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/update-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - message: `Updated docker host (${host.id})`, - }); - expect(mockDb.updateDockerHost).toHaveBeenCalledWith(host); - - recordTestResult({ - name: "update-host success", - suite: "Docker Config - Update Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "update-host success", - suite: "Docker Config - Update Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with update confirmation", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle error when update fails", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const host: DockerHost = { - id: 2, - name: "Host2", - hostAddress: "x", - secure: false, - }; - - mockDb.updateDockerHost.mockImplementationOnce(() => { - throw new Error("Update error"); - }); - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/update-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - error: expect.any(String), - }); - - recordTestResult({ - name: "update-host failure", - suite: "Docker Config - Update Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "update-host failure", - suite: "Docker Config - Update Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "400 Error with error details", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("GET /docker-config/hosts", () => { - it("should retrieve list of hosts", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const hosts: DockerHost[] = [ - { id: 1, name: "H1", hostAddress: "a", secure: false }, - ]; - - mockDb.getDockerHosts.mockImplementation(() => hosts as never[]); - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/hosts"); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toEqual(hosts); - - recordTestResult({ - name: "get-hosts success", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "get-hosts success", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with hosts array", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle error when retrieval fails", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - mockDb.getDockerHosts.mockImplementationOnce(() => { - throw new Error("Fetch error"); - }); - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/hosts"); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - error: expect.any(String), - }); - - recordTestResult({ - name: "get-hosts failure", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "get-hosts failure", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "400 Error with error details", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("DELETE /docker-config/hosts/:id", () => { - it("should delete a host successfully", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const id = 5; - - try { - const app = createApp(); - const req = new Request(`http://localhost/docker-config/hosts/${id}`, { - method: "DELETE", - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - message: `Deleted docker host (${id})`, - }); - expect(mockDb.deleteDockerHost).toHaveBeenCalledWith(id); - - recordTestResult({ - name: "delete-host success", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "delete-host success", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with deletion confirmation", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle error when delete fails", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const id = 6; - - mockDb.deleteDockerHost.mockImplementationOnce(() => { - throw new Error("Delete error"); - }); - - try { - const app = createApp(); - const req = new Request(`http://localhost/docker-config/hosts/${id}`, { - method: "DELETE", - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - error: expect.any(String), - }); - - recordTestResult({ - name: "delete-host failure", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "delete-host failure", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "400 Error with error details", - received: context?.response, - }, - }); - throw error; - } - }); - }); + beforeEach(() => { + mockDb.addDockerHost.mockClear(); + mockDb.updateDockerHost.mockClear(); + mockDb.getDockerHosts.mockClear(); + mockDb.deleteDockerHost.mockClear(); + }); + + describe("POST /docker-config/add-host", () => { + it("should add a docker host successfully", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + name: "Host1", + hostAddress: "127.0.0.1:2375", + secure: false, + }; + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/add-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + message: `Added docker host (${host.name})`, + }); + expect(mockDb.addDockerHost).toHaveBeenCalledWith(host); + + recordTestResult({ + name: "add-host success", + suite: "Docker Config - Add Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "add-host success", + suite: "Docker Config - Add Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with success message", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when adding a docker host fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + name: "Host2", + hostAddress: "invalid", + secure: true, + }; + + // Set mock implementation + mockDb.addDockerHost.mockImplementationOnce(() => { + throw new Error("DB error"); + }); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/add-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + error: expect.any(String), + }); + + recordTestResult({ + name: "add-host failure", + suite: "Docker Config - Add Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "add-host failure", + suite: "Docker Config - Add Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error structure", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("POST /docker-config/update-host", () => { + it("should update a docker host successfully", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + id: 1, + name: "Host1-upd", + hostAddress: "127.0.0.1:2376", + secure: true, + }; + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/update-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + message: `Updated docker host (${host.id})`, + }); + expect(mockDb.updateDockerHost).toHaveBeenCalledWith(host); + + recordTestResult({ + name: "update-host success", + suite: "Docker Config - Update Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "update-host success", + suite: "Docker Config - Update Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with update confirmation", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when update fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + id: 2, + name: "Host2", + hostAddress: "x", + secure: false, + }; + + mockDb.updateDockerHost.mockImplementationOnce(() => { + throw new Error("Update error"); + }); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/update-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + error: expect.any(String), + }); + + recordTestResult({ + name: "update-host failure", + suite: "Docker Config - Update Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "update-host failure", + suite: "Docker Config - Update Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error details", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("GET /docker-config/hosts", () => { + it("should retrieve list of hosts", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const hosts: DockerHost[] = [ + { id: 1, name: "H1", hostAddress: "a", secure: false }, + ]; + + mockDb.getDockerHosts.mockImplementation(() => hosts as never[]); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/hosts"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toEqual(hosts); + + recordTestResult({ + name: "get-hosts success", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "get-hosts success", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with hosts array", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when retrieval fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + mockDb.getDockerHosts.mockImplementationOnce(() => { + throw new Error("Fetch error"); + }); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/hosts"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + error: expect.any(String), + }); + + recordTestResult({ + name: "get-hosts failure", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "get-hosts failure", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error details", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("DELETE /docker-config/hosts/:id", () => { + it("should delete a host successfully", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const id = 5; + + try { + const app = createApp(); + const req = new Request(`http://localhost/docker-config/hosts/${id}`, { + method: "DELETE", + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + message: `Deleted docker host (${id})`, + }); + expect(mockDb.deleteDockerHost).toHaveBeenCalledWith(id); + + recordTestResult({ + name: "delete-host success", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "delete-host success", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with deletion confirmation", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when delete fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const id = 6; + + mockDb.deleteDockerHost.mockImplementationOnce(() => { + throw new Error("Delete error"); + }); + + try { + const app = createApp(); + const req = new Request(`http://localhost/docker-config/hosts/${id}`, { + method: "DELETE", + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + error: expect.any(String), + }); + + recordTestResult({ + name: "delete-host failure", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "delete-host failure", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error details", + received: context?.response, + }, + }); + throw error; + } + }); + }); }); afterAll(() => { - generateJunitReport(); + generateJunitReport(); }); diff --git a/tsconfig.json b/tsconfig.json index fc07b9b..dad4550 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -61,8 +61,8 @@ // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ // "newLine": "crlf", /* Set the newline character for emitting files. */ // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ From 4b52cb2070e052c430d8f5da55030f5beb275550 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 14 May 2025 14:20:34 +0000 Subject: [PATCH 307/324] Update dependency graphs --- dependency-graph.mmd | 5 +- dependency-graph.svg | 1057 +++++++++++++++++++++--------------------- 2 files changed, 519 insertions(+), 543 deletions(-) diff --git a/dependency-graph.mmd b/dependency-graph.mmd index 8567428..affe047 100644 --- a/dependency-graph.mmd +++ b/dependency-graph.mmd @@ -17,7 +17,6 @@ X["live-logs.ts"] 1M["docker-websocket.ts"] 1O["logs.ts"] 1P["stacks.ts"] -1S["utils.ts"] end subgraph 9["core"] subgraph A["utils"] @@ -99,7 +98,6 @@ M["os"] 1-->X 1-->1O 1-->1P -1-->1S 1-->4 1-->5 7-->B @@ -223,6 +221,7 @@ Z-->S 1P-->1R 1P-->B 1P-->1J +1P-->4 1R-->W 1R-->F 1R-->B @@ -230,6 +229,4 @@ Z-->S 1R-->4 1R-->V 1R-->L -1S-->1C -1S-->1J diff --git a/dependency-graph.svg b/dependency-graph.svg index abb59e5..8e7e54b 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -4,77 +4,77 @@ - - + + dependency-cruiser output - + cluster_fs - -fs + +fs cluster_src - -src + +src cluster_src/core - -core + +core cluster_src/core/database - -database + +database cluster_src/core/docker - -docker + +docker cluster_src/core/plugins - -plugins + +plugins cluster_src/core/stacks - -stacks + +stacks cluster_src/core/utils - -utils + +utils cluster_src/middleware - -middleware + +middleware cluster_src/routes - -routes + +routes cluster_~ - -~ + +~ cluster_~/typings - -typings + +typings bun:sqlite - -bun:sqlite + +bun:sqlite @@ -91,8 +91,8 @@ events - -events + +events @@ -100,8 +100,8 @@ fs - -fs + +fs @@ -109,8 +109,8 @@ fs/promises - -promises + +promises @@ -118,8 +118,8 @@ os - -os + +os @@ -127,8 +127,8 @@ package.json - -package.json + +package.json @@ -136,8 +136,8 @@ path - -path + +path @@ -145,8 +145,8 @@ src/core/database/_dbState.ts - -_dbState.ts + +_dbState.ts @@ -154,902 +154,902 @@ src/core/database/backup.ts - -backup.ts + +backup.ts src/core/database/backup.ts->fs - - + + src/core/database/backup.ts->src/core/database/_dbState.ts - - + + src/core/database/database.ts - -database.ts + +database.ts src/core/database/backup.ts->src/core/database/database.ts - - + + src/core/database/helper.ts - -helper.ts + +helper.ts src/core/database/backup.ts->src/core/database/helper.ts - - - - + + + + src/core/utils/logger.ts - -logger.ts + +logger.ts src/core/database/backup.ts->src/core/utils/logger.ts - - - - + + + + ~/typings/misc - -misc + +misc src/core/database/backup.ts->~/typings/misc - - + + src/core/database/database.ts->bun:sqlite - - + + src/core/database/database.ts->fs - - + + src/core/database/database.ts->fs/promises - - + + src/core/database/database.ts->os - - + + src/core/database/database.ts->path - - + + src/core/database/helper.ts->src/core/database/_dbState.ts - - + + src/core/database/helper.ts->src/core/utils/logger.ts - - - - + + + + src/core/utils/logger.ts->path - - + + src/core/utils/logger.ts->src/core/database/_dbState.ts - - + + src/core/database/index.ts - -index.ts + +index.ts src/core/utils/logger.ts->src/core/database/index.ts - - - - + + + + ~/typings/database - -database + +database src/core/utils/logger.ts->~/typings/database - - + + src/routes/live-logs.ts - -live-logs.ts + +live-logs.ts src/core/utils/logger.ts->src/routes/live-logs.ts - - - - + + + + src/core/database/config.ts - -config.ts + +config.ts src/core/database/config.ts->src/core/database/database.ts - - + + src/core/database/config.ts->src/core/database/helper.ts - - - - + + + + src/core/database/containerStats.ts - -containerStats.ts + +containerStats.ts src/core/database/containerStats.ts->src/core/database/database.ts - - + + src/core/database/containerStats.ts->src/core/database/helper.ts - - - - + + + + src/core/database/dockerHosts.ts - -dockerHosts.ts + +dockerHosts.ts src/core/database/dockerHosts.ts->src/core/database/database.ts - - + + src/core/database/dockerHosts.ts->src/core/database/helper.ts - - - - + + + + src/core/database/hostStats.ts - -hostStats.ts + +hostStats.ts src/core/database/hostStats.ts->src/core/database/database.ts - - + + src/core/database/hostStats.ts->src/core/database/helper.ts - - - - + + + + ~/typings/docker - -docker + +docker src/core/database/hostStats.ts->~/typings/docker - - + + src/core/database/index.ts->src/core/database/backup.ts - - - - + + + + src/core/database/index.ts->src/core/database/database.ts - - + + src/core/database/index.ts->src/core/database/config.ts - - - - + + + + src/core/database/index.ts->src/core/database/containerStats.ts - - - - + + + + src/core/database/index.ts->src/core/database/dockerHosts.ts - - - - + + + + src/core/database/index.ts->src/core/database/hostStats.ts - - - - + + + + src/core/database/logs.ts - -logs.ts + +logs.ts src/core/database/index.ts->src/core/database/logs.ts - - - - + + + + src/core/database/stacks.ts - -stacks.ts + +stacks.ts src/core/database/index.ts->src/core/database/stacks.ts - - - - + + + + src/core/database/logs.ts->src/core/database/database.ts - - + + src/core/database/logs.ts->src/core/database/helper.ts - - - - + + + + src/core/database/logs.ts->~/typings/database - - + + src/core/database/stacks.ts->src/core/database/database.ts - - + + src/core/database/stacks.ts->src/core/database/helper.ts - - - - + + + + src/core/database/stacks.ts->~/typings/database - - + + src/core/utils/helpers.ts - -helpers.ts + +helpers.ts src/core/database/stacks.ts->src/core/utils/helpers.ts - - - - + + + + ~/typings/docker-compose - -docker-compose + +docker-compose src/core/database/stacks.ts->~/typings/docker-compose - - + + src/core/utils/helpers.ts->src/core/utils/logger.ts - - - - + + + + src/core/docker/client.ts - -client.ts + +client.ts src/core/docker/client.ts->src/core/utils/logger.ts - - + + src/core/docker/client.ts->~/typings/docker - - + + src/core/docker/monitor.ts - -monitor.ts + +monitor.ts src/core/docker/monitor.ts->src/core/utils/logger.ts - - + + src/core/docker/monitor.ts->~/typings/docker - - + + src/core/docker/monitor.ts->src/core/database/index.ts - - + + src/core/docker/monitor.ts->src/core/docker/client.ts - - + + src/core/plugins/plugin-manager.ts - -plugin-manager.ts + +plugin-manager.ts src/core/docker/monitor.ts->src/core/plugins/plugin-manager.ts - - + + src/core/plugins/plugin-manager.ts->events - - + + src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts - - + + src/core/plugins/plugin-manager.ts->~/typings/docker - - + + ~/typings/plugin - -plugin + +plugin src/core/plugins/plugin-manager.ts->~/typings/plugin - - + + src/core/docker/scheduler.ts - -scheduler.ts + +scheduler.ts src/core/docker/scheduler.ts->src/core/utils/logger.ts - - + + src/core/docker/scheduler.ts->src/core/database/index.ts - - + + src/core/docker/scheduler.ts->~/typings/database - - + + src/core/docker/store-container-stats.ts - -store-container-stats.ts + +store-container-stats.ts src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts - - + + src/core/docker/store-host-stats.ts - -store-host-stats.ts + +store-host-stats.ts src/core/docker/scheduler.ts->src/core/docker/store-host-stats.ts - - + + src/core/docker/store-container-stats.ts->src/core/utils/logger.ts - - + + src/core/docker/store-container-stats.ts->src/core/database/index.ts - - + + src/core/docker/store-container-stats.ts->src/core/docker/client.ts - - + + src/core/utils/calculations.ts - -calculations.ts + +calculations.ts src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts - - + + src/core/docker/store-host-stats.ts->src/core/utils/logger.ts - - + + src/core/docker/store-host-stats.ts->~/typings/docker - - + + src/core/docker/store-host-stats.ts->src/core/database/index.ts - - + + src/core/docker/store-host-stats.ts->src/core/utils/helpers.ts - - + + src/core/docker/store-host-stats.ts->src/core/docker/client.ts - - + + ~/typings/dockerode - -dockerode + +dockerode src/core/docker/store-host-stats.ts->~/typings/dockerode - - + + src/core/plugins/loader.ts - -loader.ts + +loader.ts src/core/plugins/loader.ts->fs - - + + src/core/plugins/loader.ts->path - - + + src/core/plugins/loader.ts->src/core/utils/logger.ts - - + + src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts - - + + src/core/utils/change-me-checker.ts - -change-me-checker.ts + +change-me-checker.ts src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts - - + + src/core/utils/change-me-checker.ts->fs/promises - - + + src/core/utils/change-me-checker.ts->src/core/utils/logger.ts - - + + src/core/stacks/controller.ts - -controller.ts + +controller.ts src/core/stacks/controller.ts->fs/promises - - + + src/core/stacks/controller.ts->src/core/utils/logger.ts - - + + src/core/stacks/controller.ts->src/core/database/index.ts - - + + src/core/stacks/controller.ts->~/typings/database - - + + src/core/stacks/controller.ts->src/core/utils/helpers.ts - - + + src/core/stacks/controller.ts->~/typings/docker-compose - - + + src/routes/live-stacks.ts - -live-stacks.ts + +live-stacks.ts src/core/stacks/controller.ts->src/routes/live-stacks.ts - - + + - + src/routes/live-stacks.ts->src/core/utils/logger.ts - - + + - + ~/typings/websocket - - -websocket + + +websocket - + src/routes/live-stacks.ts->~/typings/websocket - - + + - + src/routes/live-logs.ts->src/core/utils/logger.ts - - - - + + + + - + src/routes/live-logs.ts->~/typings/database - - + + src/core/utils/package-json.ts - -package-json.ts + +package-json.ts src/core/utils/package-json.ts->package.json - - + + src/core/utils/response-handler.ts - -response-handler.ts + +response-handler.ts src/core/utils/response-handler.ts->src/core/utils/logger.ts - - + + ~/typings/elysiajs - -elysiajs + +elysiajs src/core/utils/response-handler.ts->~/typings/elysiajs - - + + src/core/utils/swagger-readme.ts - -swagger-readme.ts + +swagger-readme.ts @@ -1057,439 +1057,418 @@ src/index.ts - -index.ts + +index.ts - + src/index.ts->elysia-remote-dts - + src/index.ts->src/core/utils/logger.ts - - + + src/index.ts->src/core/database/index.ts - - + + - + src/index.ts->~/typings/database - - + + src/index.ts->src/core/docker/monitor.ts - - + + src/index.ts->src/core/docker/scheduler.ts - - + + src/index.ts->src/core/plugins/loader.ts - - + + src/index.ts->src/routes/live-stacks.ts - - + + src/index.ts->src/routes/live-logs.ts - - + + src/index.ts->src/core/utils/package-json.ts - - + + src/index.ts->src/core/utils/swagger-readme.ts - - + + src/middleware/auth.ts - -auth.ts + +auth.ts src/index.ts->src/middleware/auth.ts - - + + src/routes/api-config.ts - -api-config.ts + +api-config.ts src/index.ts->src/routes/api-config.ts - - + + src/routes/docker-manager.ts - -docker-manager.ts + +docker-manager.ts src/index.ts->src/routes/docker-manager.ts - - + + src/routes/docker-stats.ts - -docker-stats.ts + +docker-stats.ts src/index.ts->src/routes/docker-stats.ts - - + + src/routes/docker-websocket.ts - -docker-websocket.ts + +docker-websocket.ts src/index.ts->src/routes/docker-websocket.ts - - + + src/routes/logs.ts - -logs.ts + +logs.ts src/index.ts->src/routes/logs.ts - - + + src/routes/stacks.ts - -stacks.ts + +stacks.ts src/index.ts->src/routes/stacks.ts - - - - - -src/routes/utils.ts - - -utils.ts - - - - - -src/index.ts->src/routes/utils.ts - - + + - + src/middleware/auth.ts->src/core/utils/logger.ts - - + + - + src/middleware/auth.ts->src/core/database/index.ts - - + + - + src/middleware/auth.ts->~/typings/database - - + + - + src/middleware/auth.ts->~/typings/elysiajs - - + + - + src/routes/api-config.ts->fs - - + + - + src/routes/api-config.ts->src/core/database/backup.ts - - + + - + src/routes/api-config.ts->src/core/utils/logger.ts - - + + - + src/routes/api-config.ts->src/core/database/index.ts - - + + - + src/routes/api-config.ts->~/typings/database - - + + - + src/routes/api-config.ts->src/core/plugins/plugin-manager.ts - - + + - + src/routes/api-config.ts->src/core/utils/package-json.ts - - + + - + src/routes/api-config.ts->src/core/utils/response-handler.ts - - + + - + src/routes/api-config.ts->src/middleware/auth.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-manager.ts->~/typings/docker - - + + - + src/routes/docker-manager.ts->src/core/database/index.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/response-handler.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-stats.ts->~/typings/docker - - + + - + src/routes/docker-stats.ts->src/core/database/index.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/helpers.ts - - + + - + src/routes/docker-stats.ts->src/core/docker/client.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-stats.ts->~/typings/dockerode - - + + - + src/routes/docker-stats.ts->src/core/utils/response-handler.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-websocket.ts->src/core/database/index.ts - - + + - + src/routes/docker-websocket.ts->src/core/docker/client.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/response-handler.ts - - + + - + stream - - -stream + + +stream - + src/routes/docker-websocket.ts->stream - - + + - + src/routes/logs.ts->src/core/utils/logger.ts - - + + - + src/routes/logs.ts->src/core/database/index.ts - - + + - + src/routes/stacks.ts->src/core/utils/logger.ts - - + + - + src/routes/stacks.ts->src/core/database/index.ts - - + + + + + +src/routes/stacks.ts->~/typings/database + + - + src/routes/stacks.ts->src/core/stacks/controller.ts - - + + - + src/routes/stacks.ts->src/core/utils/response-handler.ts - - - - - -src/routes/utils.ts->src/core/utils/package-json.ts - - - - - -src/routes/utils.ts->src/core/utils/response-handler.ts - - + + From b826987124f0145dbb8a67b4424468e075752e0c Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 14 May 2025 16:26:33 +0200 Subject: [PATCH 308/324] CI/CD: Move lint into seperate workflow --- .github/workflows/ci.yml | 33 +------ .github/workflows/lint.yaml | 57 +++++++++++ dependency-graph.dot | 182 ------------------------------------ 3 files changed, 59 insertions(+), 213 deletions(-) create mode 100644 .github/workflows/lint.yaml delete mode 100644 dependency-graph.dot diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 31877af..8806b90 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,8 +7,8 @@ on: branches: ["**"] jobs: - lint-test: - name: Lint and Test + unit-test: + name: Test runs-on: ubuntu-latest permissions: contents: write @@ -27,25 +27,6 @@ jobs: - name: Install dependencies run: bun install - - name: Knip check - if: ${{ github.event_name == 'pull_request' }} - uses: codex-/knip-reporter@v2 - - - name: Run linter - run: | - bun biome format --fix - bun biome lint --fix - bun biome check --fix - bun biome ci - - - name: Add linted files - run: git add src/ - - - name: Check for changes - id: check-changes - run: | - git diff --cached --quiet || echo "changes_detected=true" >> $GITHUB_OUTPUT - - name: Run unit tests run: | export PAD_NEW_LINES=false @@ -59,16 +40,6 @@ jobs: with: report_paths: "reports/junit/*.xml" - - name: Commit and push lint changes - if: | - steps.check-changes.outputs.changes_detected == 'true' && - github.event_name == 'push' - run: | - git config --global user.name "GitHub Actions" - git config --global user.email "actions@github.com" - git commit -m "CQL: Apply lint fixes [skip ci]" - git push - build-scan: name: Build and Security Scan runs-on: ubuntu-latest diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 0000000..6a253b1 --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,57 @@ +name: Lint + +on: + push: + branches: ["**"] + pull_request: + branches: ["**"] + +jobs: + lint-test: + name: Lint + runs-on: ubuntu-latest + permissions: + contents: write + checks: write + security-events: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Knip check + if: ${{ github.event_name == 'pull_request' }} + uses: codex-/knip-reporter@v2 + + - name: Run linter + run: | + bun biome format --fix + bun biome lint --fix + bun biome check --fix + bun biome ci + + - name: Add linted files + run: git add src/ + + - name: Check for changes + id: check-changes + run: | + git diff --cached --quiet || echo "changes_detected=true" >> $GITHUB_OUTPUT + + - name: Commit and push lint changes + if: | + steps.check-changes.outputs.changes_detected == 'true' && + github.event_name == 'push' + run: | + git config --global user.name "GitHub Actions" + git config --global user.email "actions@github.com" + git commit -m "CQL: Apply lint fixes [skip ci]" + git push diff --git a/dependency-graph.dot b/dependency-graph.dot deleted file mode 100644 index 7c5bbef..0000000 --- a/dependency-graph.dot +++ /dev/null @@ -1,182 +0,0 @@ -strict digraph "dependency-cruiser output"{ - rankdir="LR" splines="true" overlap="false" nodesep="0.16" ranksep="0.18" fontname="Helvetica-bold" fontsize="9" style="rounded,bold,filled" fillcolor="#ffffff" compound="true" - node [shape="box" style="rounded, filled" height="0.2" color="black" fillcolor="#ffffcc" fontcolor="black" fontname="Helvetica" fontsize="9"] - edge [arrowhead="normal" arrowsize="0.6" penwidth="2.0" color="#00000033" fontname="Helvetica" fontsize="9"] - - "bun" [label= tooltip="bun" ] - "bun:sqlite" [label= tooltip="bun:sqlite" ] - "events" [label= tooltip="events" URL="https://nodejs.org/api/events.html" color="grey" fontcolor="grey"] - "fs" [label= tooltip="fs" URL="https://nodejs.org/api/fs.html" color="grey" fontcolor="grey"] - subgraph "cluster_fs" {label="fs" "fs/promises" [label= tooltip="promises" URL="https://nodejs.org/api/fs.html" color="grey" fontcolor="grey"] } - "package.json" [label= tooltip="package.json" URL="package.json" fillcolor="#ffee44"] - "path" [label= tooltip="path" URL="https://nodejs.org/api/path.html" color="grey" fontcolor="grey"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/config.ts" [label= tooltip="config.ts" URL="src/core/database/config.ts" fillcolor="#ddfeff"] } } } - "src/core/database/config.ts" -> "src/core/database/database.ts" - "src/core/database/config.ts" -> "src/core/database/helper.ts" [arrowhead="normalnoneodot"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/containerStats.ts" [label= tooltip="containerStats.ts" URL="src/core/database/containerStats.ts" fillcolor="#ddfeff"] } } } - "src/core/database/containerStats.ts" -> "src/core/database/database.ts" - "src/core/database/containerStats.ts" -> "src/core/database/helper.ts" [arrowhead="normalnoneodot"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/database.ts" [label= tooltip="database.ts" URL="src/core/database/database.ts" fillcolor="#ddfeff"] } } } - "src/core/database/database.ts" -> "bun:sqlite" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/dockerHosts.ts" [label= tooltip="dockerHosts.ts" URL="src/core/database/dockerHosts.ts" fillcolor="#ddfeff"] } } } - "src/core/database/dockerHosts.ts" -> "src/core/database/database.ts" - "src/core/database/dockerHosts.ts" -> "src/core/database/helper.ts" [arrowhead="normalnoneodot"] - "src/core/database/dockerHosts.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/helper.ts" [label= tooltip="helper.ts" URL="src/core/database/helper.ts" fillcolor="#ddfeff"] } } } - "src/core/database/helper.ts" -> "src/core/utils/logger.ts" [arrowhead="normalnoneodot"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/hostStats.ts" [label= tooltip="hostStats.ts" URL="src/core/database/hostStats.ts" fillcolor="#ddfeff"] } } } - "src/core/database/hostStats.ts" -> "src/core/database/database.ts" - "src/core/database/hostStats.ts" -> "src/core/database/helper.ts" [arrowhead="normalnoneodot"] - "src/core/database/hostStats.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/index.ts" [label= tooltip="index.ts" URL="src/core/database/index.ts" fillcolor="#ddfeff"] } } } - "src/core/database/index.ts" -> "src/core/database/config.ts" [arrowhead="normalnoneodot"] - "src/core/database/index.ts" -> "src/core/database/containerStats.ts" [arrowhead="normalnoneodot"] - "src/core/database/index.ts" -> "src/core/database/database.ts" - "src/core/database/index.ts" -> "src/core/database/dockerHosts.ts" [arrowhead="normalnoneodot"] - "src/core/database/index.ts" -> "src/core/database/hostStats.ts" [arrowhead="normalnoneodot"] - "src/core/database/index.ts" -> "src/core/database/logs.ts" [arrowhead="normalnoneodot"] - "src/core/database/index.ts" -> "src/core/database/stacks.ts" [arrowhead="normalnoneodot"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/logs.ts" [label= tooltip="logs.ts" URL="src/core/database/logs.ts" fillcolor="#ddfeff"] } } } - "src/core/database/logs.ts" -> "src/core/database/database.ts" - "src/core/database/logs.ts" -> "src/core/database/helper.ts" [arrowhead="normalnoneodot"] - "src/core/database/logs.ts" -> "src/typings/websocket.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/stacks.ts" [label= tooltip="stacks.ts" URL="src/core/database/stacks.ts" fillcolor="#ddfeff"] } } } - "src/core/database/stacks.ts" -> "src/core/database/database.ts" - "src/core/database/stacks.ts" -> "src/core/database/helper.ts" [arrowhead="normalnoneodot"] - "src/core/database/stacks.ts" -> "src/typings/database.ts" [arrowhead="onormal" penwidth="1.0"] - "src/core/database/stacks.ts" -> "src/typings/docker-compose.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/client.ts" [label= tooltip="client.ts" URL="src/core/docker/client.ts" fillcolor="#ddfeff"] } } } - "src/core/docker/client.ts" -> "src/core/utils/logger.ts" - "src/core/docker/client.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/monitor.ts" [label= tooltip="monitor.ts" URL="src/core/docker/monitor.ts" fillcolor="#ddfeff"] } } } - "src/core/docker/monitor.ts" -> "src/core/plugins/plugin-manager.ts" - "src/core/docker/monitor.ts" -> "src/core/database/index.ts" - "src/core/docker/monitor.ts" -> "src/core/docker/client.ts" - "src/core/docker/monitor.ts" -> "src/core/utils/logger.ts" - "src/core/docker/monitor.ts" -> "src/typings/docker.ts" - "src/core/docker/monitor.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] - "src/core/docker/monitor.ts" -> "bun" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/scheduler.ts" [label= tooltip="scheduler.ts" URL="src/core/docker/scheduler.ts" fillcolor="#ddfeff"] } } } - "src/core/docker/scheduler.ts" -> "src/core/database/index.ts" - "src/core/docker/scheduler.ts" -> "src/core/docker/store-host-stats.ts" - "src/core/docker/scheduler.ts" -> "src/core/docker/store-container-stats.ts" - "src/core/docker/scheduler.ts" -> "src/core/utils/logger.ts" - "src/core/docker/scheduler.ts" -> "src/typings/database.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/store-container-stats.ts" [label= tooltip="store-container-stats.ts" URL="src/core/docker/store-container-stats.ts" fillcolor="#ddfeff"] } } } - "src/core/docker/store-container-stats.ts" -> "src/core/utils/logger.ts" - "src/core/docker/store-container-stats.ts" -> "src/core/database/index.ts" - "src/core/docker/store-container-stats.ts" -> "src/core/docker/client.ts" - "src/core/docker/store-container-stats.ts" -> "src/core/utils/calculations.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/store-host-stats.ts" [label= tooltip="store-host-stats.ts" URL="src/core/docker/store-host-stats.ts" fillcolor="#ddfeff"] } } } - "src/core/docker/store-host-stats.ts" -> "src/core/database/index.ts" - "src/core/docker/store-host-stats.ts" -> "src/core/docker/client.ts" - "src/core/docker/store-host-stats.ts" -> "src/core/utils/logger.ts" - "src/core/docker/store-host-stats.ts" -> "src/typings/docker.ts" - "src/core/docker/store-host-stats.ts" -> "src/typings/dockerode.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/plugins" {label="plugins" "src/core/plugins/loader.ts" [label= tooltip="loader.ts" URL="src/core/plugins/loader.ts" fillcolor="#ddfeff"] } } } - "src/core/plugins/loader.ts" -> "src/core/utils/change-me-checker.ts" - "src/core/plugins/loader.ts" -> "src/core/utils/logger.ts" - "src/core/plugins/loader.ts" -> "src/core/plugins/plugin-manager.ts" - "src/core/plugins/loader.ts" -> "fs" [style="dashed" penwidth="1.0"] - "src/core/plugins/loader.ts" -> "path" [style="dashed" penwidth="1.0"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/plugins" {label="plugins" "src/core/plugins/plugin-manager.ts" [label= tooltip="plugin-manager.ts" URL="src/core/plugins/plugin-manager.ts" fillcolor="#ddfeff"] } } } - "src/core/plugins/plugin-manager.ts" -> "src/core/utils/logger.ts" - "src/core/plugins/plugin-manager.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] - "src/core/plugins/plugin-manager.ts" -> "src/typings/plugin.ts" [arrowhead="onormal" penwidth="1.0"] - "src/core/plugins/plugin-manager.ts" -> "events" [style="dashed" penwidth="1.0"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/stacks" {label="stacks" "src/core/stacks/controller.ts" [label= tooltip="controller.ts" URL="src/core/stacks/controller.ts" fillcolor="#ddfeff"] } } } - "src/core/stacks/controller.ts" -> "src/core/database/index.ts" - "src/core/stacks/controller.ts" -> "src/core/utils/logger.ts" - "src/core/stacks/controller.ts" -> "src/typings/database.ts" [arrowhead="onormal" penwidth="1.0"] - "src/core/stacks/controller.ts" -> "src/typings/docker-compose.ts" [arrowhead="onormal" penwidth="1.0"] - "src/core/stacks/controller.ts" -> "bun" - "src/core/stacks/controller.ts" -> "fs/promises" [style="dashed" penwidth="1.0"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/calculations.ts" [label= tooltip="calculations.ts" URL="src/core/utils/calculations.ts" fillcolor="#ddfeff"] } } } - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/change-me-checker.ts" [label= tooltip="change-me-checker.ts" URL="src/core/utils/change-me-checker.ts" fillcolor="#ddfeff"] } } } - "src/core/utils/change-me-checker.ts" -> "src/core/utils/logger.ts" - "src/core/utils/change-me-checker.ts" -> "fs/promises" [style="dashed" penwidth="1.0"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/logger.ts" [label= tooltip="logger.ts" URL="src/core/utils/logger.ts" fillcolor="#ddfeff"] } } } - "src/core/utils/logger.ts" -> "src/core/database/index.ts" [arrowhead="normalnoneodot"] - "src/core/utils/logger.ts" -> "src/routes/live-logs.ts" [arrowhead="normalnoneodot"] - "src/core/utils/logger.ts" -> "src/typings/websocket.ts" [arrowhead="onormal" penwidth="1.0"] - "src/core/utils/logger.ts" -> "path" [style="dashed" penwidth="1.0"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/package-json.ts" [label= tooltip="package-json.ts" URL="src/core/utils/package-json.ts" fillcolor="#ddfeff"] } } } - "src/core/utils/package-json.ts" -> "package.json" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/response-handler.ts" [label= tooltip="response-handler.ts" URL="src/core/utils/response-handler.ts" fillcolor="#ddfeff"] } } } - "src/core/utils/response-handler.ts" -> "src/core/utils/logger.ts" - "src/core/utils/response-handler.ts" -> "src/typings/elysiajs.ts" [arrowhead="onormal" penwidth="1.0"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/swagger-readme.ts" [label= tooltip="swagger-readme.ts" URL="src/core/utils/swagger-readme.ts" fillcolor="#ddfeff"] } } } - subgraph "cluster_src" {label="src" "src/index.ts" [label= tooltip="index.ts" URL="src/index.ts" fillcolor="#ddfeff"] } - "src/index.ts" -> "src/core/docker/monitor.ts" - "src/index.ts" -> "src/core/utils/swagger-readme.ts" - "src/index.ts" -> "src/middleware/auth.ts" - "src/index.ts" -> "src/routes/live-logs.ts" - "src/index.ts" -> "src/routes/stacks.ts" - "src/index.ts" -> "src/routes/utils.ts" - "src/index.ts" -> "src/typings/database.ts" - "src/index.ts" -> "src/core/database/index.ts" - "src/index.ts" -> "src/core/docker/scheduler.ts" - "src/index.ts" -> "src/core/plugins/loader.ts" - "src/index.ts" -> "src/core/utils/logger.ts" - "src/index.ts" -> "src/routes/api-config.ts" - "src/index.ts" -> "src/routes/docker-manager.ts" - "src/index.ts" -> "src/routes/docker-stats.ts" - "src/index.ts" -> "src/routes/docker-websocket.ts" - "src/index.ts" -> "src/routes/logs.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/middleware" {label="middleware" "src/middleware/auth.ts" [label= tooltip="auth.ts" URL="src/middleware/auth.ts" fillcolor="#ddfeff"] } } - "src/middleware/auth.ts" -> "src/core/database/index.ts" - "src/middleware/auth.ts" -> "src/core/utils/logger.ts" - "src/middleware/auth.ts" -> "src/typings/database.ts" - "src/middleware/auth.ts" -> "src/typings/elysiajs.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/api-config.ts" [label= tooltip="api-config.ts" URL="src/routes/api-config.ts" fillcolor="#ddfeff"] } } - "src/routes/api-config.ts" -> "src/core/database/index.ts" - "src/routes/api-config.ts" -> "src/core/plugins/plugin-manager.ts" - "src/routes/api-config.ts" -> "src/core/utils/logger.ts" - "src/routes/api-config.ts" -> "src/core/utils/package-json.ts" - "src/routes/api-config.ts" -> "src/core/utils/response-handler.ts" - "src/routes/api-config.ts" -> "src/middleware/auth.ts" - "src/routes/api-config.ts" -> "src/typings/database.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/docker-manager.ts" [label= tooltip="docker-manager.ts" URL="src/routes/docker-manager.ts" fillcolor="#ddfeff"] } } - "src/routes/docker-manager.ts" -> "src/core/database/index.ts" - "src/routes/docker-manager.ts" -> "src/core/utils/logger.ts" - "src/routes/docker-manager.ts" -> "src/core/utils/response-handler.ts" - "src/routes/docker-manager.ts" -> "src/typings/docker.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/docker-stats.ts" [label= tooltip="docker-stats.ts" URL="src/routes/docker-stats.ts" fillcolor="#ddfeff"] } } - "src/routes/docker-stats.ts" -> "src/core/database/index.ts" - "src/routes/docker-stats.ts" -> "src/core/docker/client.ts" - "src/routes/docker-stats.ts" -> "src/core/utils/calculations.ts" - "src/routes/docker-stats.ts" -> "src/core/utils/logger.ts" - "src/routes/docker-stats.ts" -> "src/core/utils/response-handler.ts" - "src/routes/docker-stats.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] - "src/routes/docker-stats.ts" -> "src/typings/dockerode.ts" [arrowhead="onormal" penwidth="1.0"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/docker-websocket.ts" [label= tooltip="docker-websocket.ts" URL="src/routes/docker-websocket.ts" fillcolor="#ddfeff"] } } - "src/routes/docker-websocket.ts" -> "src/core/database/index.ts" - "src/routes/docker-websocket.ts" -> "src/core/docker/client.ts" - "src/routes/docker-websocket.ts" -> "src/core/utils/calculations.ts" - "src/routes/docker-websocket.ts" -> "src/core/utils/logger.ts" - "src/routes/docker-websocket.ts" -> "src/core/utils/response-handler.ts" - "src/routes/docker-websocket.ts" -> "stream" [style="dashed" penwidth="1.0" arrowhead="onormal"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/live-logs.ts" [label= tooltip="live-logs.ts" URL="src/routes/live-logs.ts" fillcolor="#ddfeff"] } } - "src/routes/live-logs.ts" -> "src/core/utils/logger.ts" [arrowhead="normalnoneodot"] - "src/routes/live-logs.ts" -> "src/typings/websocket.ts" [arrowhead="onormal" penwidth="1.0"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/logs.ts" [label= tooltip="logs.ts" URL="src/routes/logs.ts" fillcolor="#ddfeff"] } } - "src/routes/logs.ts" -> "src/core/database/index.ts" - "src/routes/logs.ts" -> "src/core/utils/logger.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/stacks.ts" [label= tooltip="stacks.ts" URL="src/routes/stacks.ts" fillcolor="#ddfeff"] } } - "src/routes/stacks.ts" -> "src/core/database/index.ts" - "src/routes/stacks.ts" -> "src/core/stacks/controller.ts" - "src/routes/stacks.ts" -> "src/core/utils/logger.ts" - "src/routes/stacks.ts" -> "src/core/utils/response-handler.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/utils.ts" [label= tooltip="utils.ts" URL="src/routes/utils.ts" fillcolor="#ddfeff"] } } - "src/routes/utils.ts" -> "src/core/utils/package-json.ts" - "src/routes/utils.ts" -> "src/core/utils/response-handler.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/database.ts" [label= tooltip="database.ts" URL="src/typings/database.ts" fillcolor="#ddfeff"] } } - subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/docker-compose.ts" [label= tooltip="docker-compose.ts" URL="src/typings/docker-compose.ts" fillcolor="#ddfeff"] } } - subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/docker.ts" [label= tooltip="docker.ts" URL="src/typings/docker.ts" fillcolor="#ddfeff"] } } - subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/dockerode.ts" [label= tooltip="dockerode.ts" URL="src/typings/dockerode.ts" fillcolor="#ddfeff"] } } - subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/elysiajs.ts" [label= tooltip="elysiajs.ts" URL="src/typings/elysiajs.ts" fillcolor="#ddfeff"] } } - subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/plugin.ts" [label= tooltip="plugin.ts" URL="src/typings/plugin.ts" fillcolor="#ddfeff"] } } - "src/typings/plugin.ts" -> "src/typings/docker.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/websocket.ts" [label= tooltip="websocket.ts" URL="src/typings/websocket.ts" fillcolor="#ddfeff"] } } - "stream" [label= tooltip="stream" URL="https://nodejs.org/api/stream.html" color="grey" fontcolor="grey"] -} From 7a3253d981de9fb8f45d4684eb4f4843e11700ad Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Wed, 14 May 2025 14:26:56 +0000 Subject: [PATCH 309/324] CQL: Apply lint fixes [skip ci] --- src/core/stacks/controller.ts | 682 +++++++++++++++++----------------- src/routes/live-stacks.ts | 34 +- 2 files changed, 358 insertions(+), 358 deletions(-) diff --git a/src/core/stacks/controller.ts b/src/core/stacks/controller.ts index 95a6480..1f506bf 100644 --- a/src/core/stacks/controller.ts +++ b/src/core/stacks/controller.ts @@ -9,393 +9,393 @@ import type { ComposeSpec, Stack } from "~/typings/docker-compose"; import { findObjectByKey } from "../utils/helpers"; const wrapProgressCallback = (progressCallback?: (log: string) => void) => { - return progressCallback - ? (chunk: Buffer, streamSource?: "stdout" | "stderr") => { - const log = chunk.toString(); - progressCallback(log); - } - : undefined; + return progressCallback + ? (chunk: Buffer, streamSource?: "stdout" | "stderr") => { + const log = chunk.toString(); + progressCallback(log); + } + : undefined; }; async function getStackName(stack_id: number): Promise { - logger.debug(`Fetching stack name for id ${stack_id}`); - const stacks = dbFunctions.getStacks(); - const stack = findObjectByKey(stacks, "id", stack_id); - if (!stack) { - throw new Error(`Stack with id ${stack_id} not found`); - } - return stack.name; + logger.debug(`Fetching stack name for id ${stack_id}`); + const stacks = dbFunctions.getStacks(); + const stack = findObjectByKey(stacks, "id", stack_id); + if (!stack) { + throw new Error(`Stack with id ${stack_id} not found`); + } + return stack.name; } async function runStackCommand( - stack_id: number, - command: ( - cwd: string, - progressCallback?: (log: string) => void - ) => Promise, - action: string + stack_id: number, + command: ( + cwd: string, + progressCallback?: (log: string) => void, + ) => Promise, + action: string, ): Promise { - try { - logger.debug( - `Starting runStackCommand for stack_id=${stack_id}, action="${action}"` - ); - - const stackName = await getStackName(stack_id); - logger.debug( - `Retrieved stack name "${stackName}" for stack_id=${stack_id}` - ); - - const stackPath = await getStackPath({ - id: stack_id, - name: stackName, - } as Stack); - logger.debug(`Resolved stack path "${stackPath}" for stack_id=${stack_id}`); - - const progressCallback = (log: string) => { - const message = log.trim(); - logger.debug( - `Progress for stack_id=${stack_id}, action="${action}": ${message}` - ); - - // ERROR HANDLING FOR COMPOSE ACTIONS - if (message.includes("Error response from daemon")) { - logger.error( - `Error response from daemon: ${ - message.split("Error response from daemon:")[1] - }` - ); - } - - postToClient({ - type: "stack-progress", - data: { - stack_id, - action, - message, - timestamp: new Date().toISOString(), - }, - }); - }; - - logger.debug( - `Executing command for stack_id=${stack_id}, action="${action}"` - ); - const result = await command(stackPath, progressCallback); - logger.debug( - `Successfully completed command for stack_id=${stack_id}, action="${action}"` - ); - - return result; - } catch (error) { - logger.debug( - `Error occurred for stack_id=${stack_id}, action="${action}": ${String( - error - )}` - ); - postToClient({ - type: "stack-error", - data: { - stack_id, - action, - message: String(error), - timestamp: new Date().toISOString(), - }, - }); - throw new Error( - `Error while ${action} stack "${stack_id}": ${String(error)}` - ); - } + try { + logger.debug( + `Starting runStackCommand for stack_id=${stack_id}, action="${action}"`, + ); + + const stackName = await getStackName(stack_id); + logger.debug( + `Retrieved stack name "${stackName}" for stack_id=${stack_id}`, + ); + + const stackPath = await getStackPath({ + id: stack_id, + name: stackName, + } as Stack); + logger.debug(`Resolved stack path "${stackPath}" for stack_id=${stack_id}`); + + const progressCallback = (log: string) => { + const message = log.trim(); + logger.debug( + `Progress for stack_id=${stack_id}, action="${action}": ${message}`, + ); + + // ERROR HANDLING FOR COMPOSE ACTIONS + if (message.includes("Error response from daemon")) { + logger.error( + `Error response from daemon: ${ + message.split("Error response from daemon:")[1] + }`, + ); + } + + postToClient({ + type: "stack-progress", + data: { + stack_id, + action, + message, + timestamp: new Date().toISOString(), + }, + }); + }; + + logger.debug( + `Executing command for stack_id=${stack_id}, action="${action}"`, + ); + const result = await command(stackPath, progressCallback); + logger.debug( + `Successfully completed command for stack_id=${stack_id}, action="${action}"`, + ); + + return result; + } catch (error) { + logger.debug( + `Error occurred for stack_id=${stack_id}, action="${action}": ${String( + error, + )}`, + ); + postToClient({ + type: "stack-error", + data: { + stack_id, + action, + message: String(error), + timestamp: new Date().toISOString(), + }, + }); + throw new Error( + `Error while ${action} stack "${stack_id}": ${String(error)}`, + ); + } } async function getStackPath(stack: Stack): Promise { - const stackName = stack.name.trim().replace(/\s+/g, "_"); - const stackId = stack.id; + const stackName = stack.name.trim().replace(/\s+/g, "_"); + const stackId = stack.id; - if (!stackId) { - logger.error("Stack could not be parsed"); - throw new Error("Stack could not be parsed"); - } + if (!stackId) { + logger.error("Stack could not be parsed"); + throw new Error("Stack could not be parsed"); + } - return `stacks/${stackId}-${stackName}`; + return `stacks/${stackId}-${stackName}`; } async function createStackYAML(compose_spec: Stack): Promise { - const yaml = YAML.stringify(compose_spec.compose_spec); - const stackPath = await getStackPath(compose_spec); - await Bun.write(`${stackPath}/docker-compose.yaml`, yaml, { - createPath: true, - }); + const yaml = YAML.stringify(compose_spec.compose_spec); + const stackPath = await getStackPath(compose_spec); + await Bun.write(`${stackPath}/docker-compose.yaml`, yaml, { + createPath: true, + }); } export async function deployStack(stack_config: stacks_config): Promise { - try { - logger.debug(`Deploying Stack: ${JSON.stringify(stack_config)}`); - - if (!stack_config.name) { - throw new Error("Stack name needed"); - } - - const jsonStringStack = { - ...stack_config, - compose_spec: JSON.stringify(stack_config.compose_spec), - }; - - const stackId = dbFunctions.addStack(jsonStringStack); - - if (!stackId) { - throw new Error("Failed to add stack to database"); - } - - postToClient({ - type: "stack-status", - data: { - stack_id: stackId, - status: "pending", - message: "Creating stack configuration", - }, - }); - - const stackYaml: Stack = { - id: stackId, - name: stack_config.name, - source: stack_config.source, - version: stack_config.version, - compose_spec: stack_config.compose_spec as unknown as ComposeSpec, // Weird stuff i am doing here... smh - }; - - await createStackYAML(stackYaml); - - await runStackCommand( - stackId, - (cwd, progressCallback) => - DockerCompose.upAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "deploying" - ); - - postToClient({ - type: "stack-status", - data: { - stack_id: stackId, - status: "deployed", - message: "Stack deployed successfully", - }, - }); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - postToClient({ - type: "stack-error", - data: { - stack_id: 0, - action: "deploying", - message: errorMsg, - timestamp: new Date().toISOString(), - }, - }); - throw new Error(errorMsg); - } + try { + logger.debug(`Deploying Stack: ${JSON.stringify(stack_config)}`); + + if (!stack_config.name) { + throw new Error("Stack name needed"); + } + + const jsonStringStack = { + ...stack_config, + compose_spec: JSON.stringify(stack_config.compose_spec), + }; + + const stackId = dbFunctions.addStack(jsonStringStack); + + if (!stackId) { + throw new Error("Failed to add stack to database"); + } + + postToClient({ + type: "stack-status", + data: { + stack_id: stackId, + status: "pending", + message: "Creating stack configuration", + }, + }); + + const stackYaml: Stack = { + id: stackId, + name: stack_config.name, + source: stack_config.source, + version: stack_config.version, + compose_spec: stack_config.compose_spec as unknown as ComposeSpec, // Weird stuff i am doing here... smh + }; + + await createStackYAML(stackYaml); + + await runStackCommand( + stackId, + (cwd, progressCallback) => + DockerCompose.upAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "deploying", + ); + + postToClient({ + type: "stack-status", + data: { + stack_id: stackId, + status: "deployed", + message: "Stack deployed successfully", + }, + }); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + postToClient({ + type: "stack-error", + data: { + stack_id: 0, + action: "deploying", + message: errorMsg, + timestamp: new Date().toISOString(), + }, + }); + throw new Error(errorMsg); + } } export async function stopStack(stack_id: number): Promise { - // Note the await to discard the result (convert to void) - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.downAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "stopping" - ); + // Note the await to discard the result (convert to void) + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.downAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "stopping", + ); } export async function startStack(stack_id: number): Promise { - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.upAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "starting" - ); + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.upAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "starting", + ); } export async function pullStackImages(stack_id: number): Promise { - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.pullAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "pulling-images" - ); + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.pullAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "pulling-images", + ); } export async function restartStack(stack_id: number): Promise { - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.restartAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "restarting" - ); + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.restartAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "restarting", + ); } export async function getStackStatus( - stack_id: number - //biome-ignore lint/suspicious/noExplicitAny: + stack_id: number, + //biome-ignore lint/suspicious/noExplicitAny: ): Promise> { - const status = await runStackCommand( - stack_id, - async (cwd) => { - const rawStatus = await DockerCompose.ps({ cwd }); - //biome-ignore lint/suspicious/noExplicitAny: - return rawStatus.data.services.reduce((acc: any, service: any) => { - acc[service.name] = service.state; - return acc; - }, {}); - }, - "status-check" - ); - return status; + const status = await runStackCommand( + stack_id, + async (cwd) => { + const rawStatus = await DockerCompose.ps({ cwd }); + //biome-ignore lint/suspicious/noExplicitAny: + return rawStatus.data.services.reduce((acc: any, service: any) => { + acc[service.name] = service.state; + return acc; + }, {}); + }, + "status-check", + ); + return status; } export async function removeStack(stack_id: number): Promise { - try { - const _ = dbFunctions.deleteStack(stack_id); - - await runStackCommand( - stack_id, - async (cwd, progressCallback) => { - await DockerCompose.down({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }); - }, - "removing" - ); - - const stackName = await getStackName(stack_id); - const stackPath = await getStackPath({ - id: stack_id, - name: stackName, - } as Stack); - - try { - await rm(stackPath, { recursive: true }); - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - postToClient({ - type: "stack-error", - data: { - stack_id, - action: "removing", - message: errorMsg, - timestamp: new Date().toISOString(), - }, - }); - throw new Error(errorMsg); - } - - postToClient({ - type: "stack-removed", - data: { - stack_id, - message: "Stack removed successfully", - }, - }); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - postToClient({ - type: "stack-error", - data: { - stack_id, - action: "removing", - message: errorMsg, - timestamp: new Date().toISOString(), - }, - }); - throw new Error(errorMsg); - } + try { + const _ = dbFunctions.deleteStack(stack_id); + + await runStackCommand( + stack_id, + async (cwd, progressCallback) => { + await DockerCompose.down({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }); + }, + "removing", + ); + + const stackName = await getStackName(stack_id); + const stackPath = await getStackPath({ + id: stack_id, + name: stackName, + } as Stack); + + try { + await rm(stackPath, { recursive: true }); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + postToClient({ + type: "stack-error", + data: { + stack_id, + action: "removing", + message: errorMsg, + timestamp: new Date().toISOString(), + }, + }); + throw new Error(errorMsg); + } + + postToClient({ + type: "stack-removed", + data: { + stack_id, + message: "Stack removed successfully", + }, + }); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + postToClient({ + type: "stack-error", + data: { + stack_id, + action: "removing", + message: errorMsg, + timestamp: new Date().toISOString(), + }, + }); + throw new Error(errorMsg); + } } interface DockerServiceStatus { - status: string; - ports: string[]; + status: string; + ports: string[]; } interface StackStatus { - services: Record; - healthy: number; - unhealthy: number; - total: number; + services: Record; + healthy: number; + unhealthy: number; + total: number; } type StacksStatus = Record; export async function getAllStacksStatus(): Promise { - try { - const stacks = dbFunctions.getStacks(); - - const statusResults = await Promise.all( - stacks.map(async (stack) => { - const status = await runStackCommand( - stack.id as number, - async (cwd) => { - const rawStatus = await DockerCompose.ps({ cwd }); - const services = rawStatus.data.services.reduce( - (acc: Record, service) => { - acc[service.name] = { - status: service.state, - ports: service.ports.map( - (port) => `${port.mapped?.address}:${port.mapped?.port}` - ), - }; - return acc; - }, - {} - ); - - const statusValues = Object.values(services); - return { - services, - healthy: statusValues.filter( - (s) => s.status === "running" || s.status.includes("Up") - ).length, - unhealthy: statusValues.filter( - (s) => s.status !== "running" && !s.status.includes("Up") - ).length, - total: statusValues.length, - }; - }, - "status-check" - ); - return { stackId: stack.id, status }; - }) - ); - - return statusResults.reduce((acc, { stackId, status }) => { - acc[String(stackId)] = status; - return acc; - }, {} as StacksStatus); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - throw new Error(errorMsg); - } + try { + const stacks = dbFunctions.getStacks(); + + const statusResults = await Promise.all( + stacks.map(async (stack) => { + const status = await runStackCommand( + stack.id as number, + async (cwd) => { + const rawStatus = await DockerCompose.ps({ cwd }); + const services = rawStatus.data.services.reduce( + (acc: Record, service) => { + acc[service.name] = { + status: service.state, + ports: service.ports.map( + (port) => `${port.mapped?.address}:${port.mapped?.port}`, + ), + }; + return acc; + }, + {}, + ); + + const statusValues = Object.values(services); + return { + services, + healthy: statusValues.filter( + (s) => s.status === "running" || s.status.includes("Up"), + ).length, + unhealthy: statusValues.filter( + (s) => s.status !== "running" && !s.status.includes("Up"), + ).length, + total: statusValues.length, + }; + }, + "status-check", + ); + return { stackId: stack.id, status }; + }), + ); + + return statusResults.reduce((acc, { stackId, status }) => { + acc[String(stackId)] = status; + return acc; + }, {} as StacksStatus); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + throw new Error(errorMsg); + } } diff --git a/src/routes/live-stacks.ts b/src/routes/live-stacks.ts index 2b5e8e3..77aa285 100644 --- a/src/routes/live-stacks.ts +++ b/src/routes/live-stacks.ts @@ -7,24 +7,24 @@ import type { stackSocketMessage } from "~/typings/websocket"; const activeConnections = new Set>(); export const liveStacks = new Elysia().ws("/stacks", { - open(ws) { - activeConnections.add(ws); - ws.send({ message: "Connection established" }); - logger.info(`New Stacks WebSocket established (${ws.id})`); - }, - close(ws) { - logger.info(`Stacks WebSocket closed (${ws.id})`); - activeConnections.delete(ws); - }, + open(ws) { + activeConnections.add(ws); + ws.send({ message: "Connection established" }); + logger.info(`New Stacks WebSocket established (${ws.id})`); + }, + close(ws) { + logger.info(`Stacks WebSocket closed (${ws.id})`); + activeConnections.delete(ws); + }, }); export function postToClient(data: stackSocketMessage) { - for (const ws of activeConnections) { - try { - ws.send(JSON.stringify(data)); - } catch (error) { - activeConnections.delete(ws); - logger.error("Failed to send to WebSocket:", error); - } - } + for (const ws of activeConnections) { + try { + ws.send(JSON.stringify(data)); + } catch (error) { + activeConnections.delete(ws); + logger.error("Failed to send to WebSocket:", error); + } + } } From 567dc2e5d0c2e577d9de0d4d10d1890292053fdb Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 14 May 2025 16:55:39 +0200 Subject: [PATCH 310/324] CI/CD: Fix readability --- .github/workflows/ci.yml | 2 -- .github/workflows/lint.yaml | 2 -- 2 files changed, 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8806b90..a3fcd84 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,7 @@ name: Continuous Integration on: push: - branches: ["**"] pull_request: - branches: ["**"] jobs: unit-test: diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 6a253b1..ca611f3 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -2,9 +2,7 @@ name: Lint on: push: - branches: ["**"] pull_request: - branches: ["**"] jobs: lint-test: From 86fb8955340222322fc4980e9140729e1fb21710 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 14 May 2025 16:59:03 +0200 Subject: [PATCH 311/324] CI/CD: Fix readability and non existent dependant test --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a3fcd84..e8da5ce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,7 @@ jobs: contents: write checks: write security-events: write + steps: - uses: actions/checkout@v4 with: @@ -41,11 +42,12 @@ jobs: build-scan: name: Build and Security Scan runs-on: ubuntu-latest - needs: lint-test + needs: unit-test permissions: contents: read checks: write security-events: write + steps: - uses: actions/checkout@v4 From f722a60542d4250a53d3b99bf2cf8e0dd1a00dfa Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 14 May 2025 17:04:56 +0200 Subject: [PATCH 312/324] CI/CD: Fix gitignore --- .github/workflows/cd.yml | 2 +- .github/workflows/ci.yml | 2 +- .github/workflows/dependency-graph.yml | 2 +- .github/workflows/lint.yaml | 2 +- .gitignore | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 1b60b66..e706110 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -1,4 +1,4 @@ -name: Continuous Delivery +name: "Continuous Delivery" on: release: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e8da5ce..8c15662 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: Continuous Integration +name: "Continuous Integration" on: push: diff --git a/.github/workflows/dependency-graph.yml b/.github/workflows/dependency-graph.yml index 4fad400..9e178ce 100644 --- a/.github/workflows/dependency-graph.yml +++ b/.github/workflows/dependency-graph.yml @@ -1,4 +1,4 @@ -name: Generate Dependency Graph +name: "Generate Dependency Graph" on: push: diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index ca611f3..5fe920a 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -1,4 +1,4 @@ -name: Lint +name: "Lint" on: push: diff --git a/.gitignore b/.gitignore index 78bc2da..9b14948 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,6 @@ build data *.xml -dependency-* +dependency-*.{mmd,dot,svg} Knip-Report.md -reports/** \ No newline at end of file +reports/** From 1854513984d69b20f39021e0633cc0929dd0e8b5 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 14 May 2025 17:07:20 +0200 Subject: [PATCH 313/324] Fix: Just commit the data folder or smth to fix this weird error... --- .gitignore | 1 - data/.gitignore | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 data/.gitignore diff --git a/.gitignore b/.gitignore index 9b14948..e34b9b1 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ /node_modules .test build -data *.xml dependency-*.{mmd,dot,svg} Knip-Report.md diff --git a/data/.gitignore b/data/.gitignore new file mode 100644 index 0000000..aed3199 --- /dev/null +++ b/data/.gitignore @@ -0,0 +1 @@ +./dockstatapi* From 5198159937023b7fad8f6bfc3a7319da852efbeb Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 14 May 2025 17:10:40 +0200 Subject: [PATCH 314/324] CI/CD: Debug junit reporter --- .github/workflows/ci.yml | 4 ++++ package.json | 3 +-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8c15662..0c0b052 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,6 +33,10 @@ jobs: bun test bun clean + - name: Log unit test files + run: | + ls -lah reports/junit + - name: Publish Test Report if: always() uses: mikepenz/action-junit-report@v5 diff --git a/package.json b/package.json index 15ffaf2..c010efb 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,7 @@ "clean:win": "node -e \"process.exit(process.platform === 'win32' ? 0 : 1)\" && cmd /c del /Q data/dockstatapi* && cmd /c del /Q reports/junit/*.xml && echo 'success'", "clean:lin": "node -e \"process.exit(process.platform !== 'win32' ? 0 : 1)\" && rm -f data/dockstatapi* && rm -f reports/junit/*.xml && echo 'success'", "knip": "knip", - "lint": "biome check --formatter-enabled=true --linter-enabled=true --organize-imports-enabled=true --fix src", - "test": "bun test src/tests/**/*.test.ts" + "lint": "biome check --formatter-enabled=true --linter-enabled=true --organize-imports-enabled=true --fix src" }, "dependencies": { "@elysiajs/server-timing": "^1.3.0", From b0edecd009dbf84db319ba63ab78c3c5dadb35c6 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 14 May 2025 17:15:00 +0200 Subject: [PATCH 315/324] CI/CD: Maybe do not delete the files before using them... --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0c0b052..5b40a3e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ on: jobs: unit-test: - name: Test + name: Unit Testing runs-on: ubuntu-latest permissions: contents: write @@ -31,7 +31,6 @@ jobs: export PAD_NEW_LINES=false docker compose -f docker/docker-compose.dev.yaml up -d bun test - bun clean - name: Log unit test files run: | From 935304bf2593519b5e699effdf5d522814a74fa4 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 14 May 2025 17:16:31 +0200 Subject: [PATCH 316/324] CI/CD: Always publish test results --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5b40a3e..f302b58 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,6 +41,7 @@ jobs: uses: mikepenz/action-junit-report@v5 with: report_paths: "reports/junit/*.xml" + include_passed: true build-scan: name: Build and Security Scan From 64c0dc80a5a4796032a5ae61d98540d574f54562 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 14 May 2025 17:26:14 +0200 Subject: [PATCH 317/324] CI/CD: New test results --- .github/workflows/ci.yml | 13 +- package.json | 4 +- src/tests/api-config.spec.ts | 638 +++++++++++------------ src/tests/docker-manager.spec.ts | 870 +++++++++++++++---------------- src/tests/junit-exporter.ts | 145 ------ src/tests/markdown-exporter.ts | 141 +++++ 6 files changed, 904 insertions(+), 907 deletions(-) delete mode 100644 src/tests/junit-exporter.ts create mode 100644 src/tests/markdown-exporter.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f302b58..5e687e4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,15 +34,16 @@ jobs: - name: Log unit test files run: | - ls -lah reports/junit + ls -lah reports/markdown - name: Publish Test Report if: always() - uses: mikepenz/action-junit-report@v5 - with: - report_paths: "reports/junit/*.xml" - include_passed: true - + run: | + SUMMARY="" + for element in $(ls reports/markdown); do + SUMMARY="$(echo -e "${SUMMARY}\n$(cat "${element}")")" + done + cho "$SUMMARY" >> $GITHUB_STEP_SUMMARY build-scan: name: Build and Security Scan runs-on: ubuntu-latest diff --git a/package.json b/package.json index c010efb..c4e322e 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,8 @@ "build:prod": "NODE_ENV=production bun build --no-native --compile --minify-whitespace --minify-syntax --target bun --outfile server ./src/index.ts", "build:docker": "docker build -f docker/Dockerfile . -t 'dockstatapi:local'", "clean": "bun run clean:win || bun run clean:lin", - "clean:win": "node -e \"process.exit(process.platform === 'win32' ? 0 : 1)\" && cmd /c del /Q data/dockstatapi* && cmd /c del /Q reports/junit/*.xml && echo 'success'", - "clean:lin": "node -e \"process.exit(process.platform !== 'win32' ? 0 : 1)\" && rm -f data/dockstatapi* && rm -f reports/junit/*.xml && echo 'success'", + "clean:win": "node -e \"process.exit(process.platform === 'win32' ? 0 : 1)\" && cmd /c del /Q data/dockstatapi* && cmd /c del /Q reports/markdown/*.md && echo 'success'", + "clean:lin": "node -e \"process.exit(process.platform !== 'win32' ? 0 : 1)\" && rm -f data/dockstatapi* && rm -f reports/markdown/*.md && echo 'success'", "knip": "knip", "lint": "biome check --formatter-enabled=true --linter-enabled=true --organize-imports-enabled=true --fix src" }, diff --git a/src/tests/api-config.spec.ts b/src/tests/api-config.spec.ts index d1f9d09..eb62cc8 100644 --- a/src/tests/api-config.spec.ts +++ b/src/tests/api-config.spec.ts @@ -2,343 +2,343 @@ import { afterAll, beforeEach, describe, expect, it, mock } from "bun:test"; import { Elysia } from "elysia"; import { logger } from "~/core/utils/logger"; import { apiConfigRoutes } from "~/routes/api-config"; -import { generateJunitReport, recordTestResult } from "./junit-exporter"; -import type { TestContext } from "./junit-exporter"; +import { generateMarkdownReport, recordTestResult } from "./markdown-exporter"; +import type { TestContext } from "./markdown-exporter"; const mockDb = { - updateConfig: mock(() => ({})), - backupDatabase: mock( - () => `dockstatapi-${new Date().toISOString().slice(0, 10)}.db.bak`, - ), - restoreDatabase: mock(), - findLatestBackup: mock(() => "dockstatapi-2025-05-06.db.bak"), + updateConfig: mock(() => ({})), + backupDatabase: mock( + () => `dockstatapi-${new Date().toISOString().slice(0, 10)}.db.bak` + ), + restoreDatabase: mock(), + findLatestBackup: mock(() => "dockstatapi-2025-05-06.db.bak"), }; mock.module("node:fs", () => ({ - existsSync: mock((path) => path.includes("dockstatapi")), - readdirSync: mock(() => [ - "dockstatapi-2025-05-06.db.bak", - "dockstatapi.db", - "dockstatapi.db-shm", - ]), - unlinkSync: mock(), + existsSync: mock((path) => path.includes("dockstatapi")), + readdirSync: mock(() => [ + "dockstatapi-2025-05-06.db.bak", + "dockstatapi.db", + "dockstatapi.db-shm", + ]), + unlinkSync: mock(), })); const mockPlugins = [ - { - name: "docker-monitor", - version: "1.2.0", - status: "active", - }, + { + name: "docker-monitor", + version: "1.2.0", + status: "active", + }, ]; const createTestApp = () => - new Elysia().use(apiConfigRoutes).decorate({ - dbFunctions: mockDb, - pluginManager: { - getLoadedPlugins: mock(() => mockPlugins), - getPlugin: mock((name) => mockPlugins.find((p) => p.name === name)), - }, - logger: { - ...logger, - debug: mock(), - error: mock(), - info: mock(), - }, - }); + new Elysia().use(apiConfigRoutes).decorate({ + dbFunctions: mockDb, + pluginManager: { + getLoadedPlugins: mock(() => mockPlugins), + getPlugin: mock((name) => mockPlugins.find((p) => p.name === name)), + }, + logger: { + ...logger, + debug: mock(), + error: mock(), + info: mock(), + }, + }); async function captureTestContext( - req: Request, - res: Response, + req: Request, + res: Response ): Promise { - const responseStatus = res.status; - const responseHeaders = Object.fromEntries(res.headers.entries()); - let responseBody: string; - - try { - responseBody = await res.clone().json(); - } catch (parseError) { - try { - responseBody = await res.clone().text(); - } catch (textError) { - responseBody = "Unparseable response content"; - } - } - - return { - request: { - method: req.method, - url: req.url, - headers: Object.fromEntries(req.headers.entries()), - body: req.body ? await req.clone().text() : undefined, - }, - response: { - status: responseStatus, - headers: responseHeaders, - body: responseBody, - }, - }; + const responseStatus = res.status; + const responseHeaders = Object.fromEntries(res.headers.entries()); + let responseBody: string; + + try { + responseBody = await res.clone().json(); + } catch (parseError) { + try { + responseBody = await res.clone().text(); + } catch (textError) { + responseBody = "Unparseable response content"; + } + } + + return { + request: { + method: req.method, + url: req.url, + headers: Object.fromEntries(req.headers.entries()), + body: req.body ? await req.clone().text() : undefined, + }, + response: { + status: responseStatus, + headers: responseHeaders, + body: responseBody, + }, + }; } describe("API Configuration Endpoints", () => { - beforeEach(() => { - mockDb.updateConfig.mockClear(); - }); - - describe("Core Configuration", () => { - it("should retrieve current config with hashed API key", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - try { - const app = createTestApp(); - const req = new Request("http://localhost:3000/config"); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - fetching_interval: expect.any(Number), - keep_data_for: expect.any(Number), - }); - - recordTestResult({ - name: "should retrieve current config with hashed API key", - suite: "API Configuration Endpoints - Core Configuration", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "should retrieve current config with hashed API key", - suite: "API Configuration Endpoints - Core Configuration", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with valid config structure", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle config update with valid payload", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - try { - const app = createTestApp(); - const requestBody = { - fetching_interval: 15, - keep_data_for: 30, - api_key: "new-valid-key", - }; - const req = new Request("http://localhost:3000/config/update", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(requestBody), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - success: true, - message: expect.stringContaining("Updated"), - }); - - recordTestResult({ - name: "should handle config update with valid payload", - suite: "API Configuration Endpoints - Core Configuration", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "should handle config update with valid payload", - suite: "API Configuration Endpoints - Core Configuration", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with update confirmation", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("Plugin Management", () => { - it("should list active plugins with metadata", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - try { - const app = createTestApp(); - const req = new Request("http://localhost:3000/config/plugins"); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toEqual( - [], - //expect.arrayContaining([ - // expect.objectContaining({ - // name: expect.any(String), - // version: expect.any(String), - // status: expect.any(String), - // }), - //]) - ); - - recordTestResult({ - name: "should list active plugins with metadata", - suite: "API Configuration Endpoints - Plugin Management", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "should list active plugins with metadata", - suite: "API Configuration Endpoints - Plugin Management", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with plugin list", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("Backup Management", () => { - it("should generate timestamped backup files", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - try { - const app = createTestApp(); - const req = new Request("http://localhost:3000/config/backup", { - method: "POST", - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - const { message } = context.response.body as { message: string }; - expect(message).toMatch( - /^data\/dockstatapi-\d{2}-\d{2}-\d{4}-1\.db\.bak$/, - ); - - recordTestResult({ - name: "should generate timestamped backup files", - suite: "API Configuration Endpoints - Backup Management", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "should generate timestamped backup files", - suite: "API Configuration Endpoints - Backup Management", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with backup path", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should list valid backup files", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - try { - const app = createTestApp(); - const req = new Request("http://localhost:3000/config/backup"); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - const backups = context.response.body as string[]; - expect(backups).toEqual( - expect.arrayContaining([expect.stringMatching(/\.db\.bak$/)]), - ); - - recordTestResult({ - name: "should list valid backup files", - suite: "API Configuration Endpoints - Backup Management", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "should list valid backup files", - suite: "API Configuration Endpoints - Backup Management", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with backup list", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("Error Handling", () => { - it("should return proper error format", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - try { - const app = createTestApp(); - const req = new Request("http://localhost:3000/random_link", { - method: "GET", - headers: { "Content-Type": "application/json" }, - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(404); - - recordTestResult({ - name: "should return proper error format", - suite: - "API Configuration Endpoints - Error Handling of unkown routes", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "should return proper error format", - suite: "API Configuration Endpoints - Error Handling", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "500 Error with structured error format", - received: context?.response, - }, - }); - throw error; - } - }); - }); + beforeEach(() => { + mockDb.updateConfig.mockClear(); + }); + + describe("Core Configuration", () => { + it("should retrieve current config with hashed API key", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const req = new Request("http://localhost:3000/config"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + fetching_interval: expect.any(Number), + keep_data_for: expect.any(Number), + }); + + recordTestResult({ + name: "should retrieve current config with hashed API key", + suite: "API Configuration Endpoints - Core Configuration", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should retrieve current config with hashed API key", + suite: "API Configuration Endpoints - Core Configuration", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with valid config structure", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle config update with valid payload", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const requestBody = { + fetching_interval: 15, + keep_data_for: 30, + api_key: "new-valid-key", + }; + const req = new Request("http://localhost:3000/config/update", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(requestBody), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + success: true, + message: expect.stringContaining("Updated"), + }); + + recordTestResult({ + name: "should handle config update with valid payload", + suite: "API Configuration Endpoints - Core Configuration", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should handle config update with valid payload", + suite: "API Configuration Endpoints - Core Configuration", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with update confirmation", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("Plugin Management", () => { + it("should list active plugins with metadata", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const req = new Request("http://localhost:3000/config/plugins"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toEqual( + [] + //expect.arrayContaining([ + // expect.objectContaining({ + // name: expect.any(String), + // version: expect.any(String), + // status: expect.any(String), + // }), + //]) + ); + + recordTestResult({ + name: "should list active plugins with metadata", + suite: "API Configuration Endpoints - Plugin Management", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should list active plugins with metadata", + suite: "API Configuration Endpoints - Plugin Management", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with plugin list", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("Backup Management", () => { + it("should generate timestamped backup files", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const req = new Request("http://localhost:3000/config/backup", { + method: "POST", + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + const { message } = context.response.body as { message: string }; + expect(message).toMatch( + /^data\/dockstatapi-\d{2}-\d{2}-\d{4}-1\.db\.bak$/ + ); + + recordTestResult({ + name: "should generate timestamped backup files", + suite: "API Configuration Endpoints - Backup Management", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should generate timestamped backup files", + suite: "API Configuration Endpoints - Backup Management", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with backup path", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should list valid backup files", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const req = new Request("http://localhost:3000/config/backup"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + const backups = context.response.body as string[]; + expect(backups).toEqual( + expect.arrayContaining([expect.stringMatching(/\.db\.bak$/)]) + ); + + recordTestResult({ + name: "should list valid backup files", + suite: "API Configuration Endpoints - Backup Management", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should list valid backup files", + suite: "API Configuration Endpoints - Backup Management", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with backup list", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("Error Handling", () => { + it("should return proper error format", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const req = new Request("http://localhost:3000/random_link", { + method: "GET", + headers: { "Content-Type": "application/json" }, + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(404); + + recordTestResult({ + name: "should return proper error format", + suite: + "API Configuration Endpoints - Error Handling of unkown routes", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should return proper error format", + suite: "API Configuration Endpoints - Error Handling", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "500 Error with structured error format", + received: context?.response, + }, + }); + throw error; + } + }); + }); }); afterAll(() => { - generateJunitReport(); + generateMarkdownReport(); }); diff --git a/src/tests/docker-manager.spec.ts b/src/tests/docker-manager.spec.ts index 962937a..2d1e6ec 100644 --- a/src/tests/docker-manager.spec.ts +++ b/src/tests/docker-manager.spec.ts @@ -3,462 +3,462 @@ import { Elysia } from "elysia"; import { dbFunctions } from "~/core/database"; import { dockerRoutes } from "~/routes/docker-manager"; import { - generateJunitReport, - recordTestResult, - testResults, -} from "./junit-exporter"; -import type { TestContext } from "./junit-exporter"; + generateMarkdownReport, + recordTestResult, + testResults, +} from "./markdown-exporter"; +import type { TestContext } from "./markdown-exporter"; type DockerHost = { - id?: number; - name: string; - hostAddress: string; - secure: boolean; + id?: number; + name: string; + hostAddress: string; + secure: boolean; }; const mockDb = { - addDockerHost: mock(() => ({ - changes: 1, - lastInsertRowid: 1, - })), - updateDockerHost: mock(() => ({ - changes: 1, - lastInsertRowid: 1, - })), - getDockerHosts: mock(() => []), - deleteDockerHost: mock(() => ({ - changes: 1, - lastInsertRowid: 1, - })), + addDockerHost: mock(() => ({ + changes: 1, + lastInsertRowid: 1, + })), + updateDockerHost: mock(() => ({ + changes: 1, + lastInsertRowid: 1, + })), + getDockerHosts: mock(() => []), + deleteDockerHost: mock(() => ({ + changes: 1, + lastInsertRowid: 1, + })), }; mock.module("~/core/database", () => ({ - dbFunctions: mockDb, + dbFunctions: mockDb, })); mock.module("~/core/utils/logger", () => ({ - logger: { - debug: mock(), - info: mock(), - error: mock(), - }, + logger: { + debug: mock(), + info: mock(), + error: mock(), + }, })); const createApp = () => new Elysia().use(dockerRoutes).decorate({}); async function captureTestContext( - req: Request, - res: Response, + req: Request, + res: Response ): Promise { - const responseStatus = res.status; - const responseHeaders = Object.fromEntries(res.headers.entries()); - let responseBody: unknown; - - try { - responseBody = await res.clone().json(); - } catch (parseError) { - try { - responseBody = await res.clone().text(); - } catch { - responseBody = "Unparseable response content"; - } - } - - return { - request: { - method: req.method, - url: req.url, - headers: Object.fromEntries(req.headers.entries()), - body: req.body ? await req.clone().text() : undefined, - }, - response: { - status: responseStatus, - headers: responseHeaders, - body: responseBody, - }, - }; + const responseStatus = res.status; + const responseHeaders = Object.fromEntries(res.headers.entries()); + let responseBody: unknown; + + try { + responseBody = await res.clone().json(); + } catch (parseError) { + try { + responseBody = await res.clone().text(); + } catch { + responseBody = "Unparseable response content"; + } + } + + return { + request: { + method: req.method, + url: req.url, + headers: Object.fromEntries(req.headers.entries()), + body: req.body ? await req.clone().text() : undefined, + }, + response: { + status: responseStatus, + headers: responseHeaders, + body: responseBody, + }, + }; } describe("Docker Configuration Endpoints", () => { - beforeEach(() => { - mockDb.addDockerHost.mockClear(); - mockDb.updateDockerHost.mockClear(); - mockDb.getDockerHosts.mockClear(); - mockDb.deleteDockerHost.mockClear(); - }); - - describe("POST /docker-config/add-host", () => { - it("should add a docker host successfully", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const host: DockerHost = { - name: "Host1", - hostAddress: "127.0.0.1:2375", - secure: false, - }; - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/add-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - message: `Added docker host (${host.name})`, - }); - expect(mockDb.addDockerHost).toHaveBeenCalledWith(host); - - recordTestResult({ - name: "add-host success", - suite: "Docker Config - Add Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "add-host success", - suite: "Docker Config - Add Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with success message", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle error when adding a docker host fails", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const host: DockerHost = { - name: "Host2", - hostAddress: "invalid", - secure: true, - }; - - // Set mock implementation - mockDb.addDockerHost.mockImplementationOnce(() => { - throw new Error("DB error"); - }); - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/add-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - error: expect.any(String), - }); - - recordTestResult({ - name: "add-host failure", - suite: "Docker Config - Add Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "add-host failure", - suite: "Docker Config - Add Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "400 Error with error structure", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("POST /docker-config/update-host", () => { - it("should update a docker host successfully", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const host: DockerHost = { - id: 1, - name: "Host1-upd", - hostAddress: "127.0.0.1:2376", - secure: true, - }; - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/update-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - message: `Updated docker host (${host.id})`, - }); - expect(mockDb.updateDockerHost).toHaveBeenCalledWith(host); - - recordTestResult({ - name: "update-host success", - suite: "Docker Config - Update Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "update-host success", - suite: "Docker Config - Update Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with update confirmation", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle error when update fails", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const host: DockerHost = { - id: 2, - name: "Host2", - hostAddress: "x", - secure: false, - }; - - mockDb.updateDockerHost.mockImplementationOnce(() => { - throw new Error("Update error"); - }); - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/update-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - error: expect.any(String), - }); - - recordTestResult({ - name: "update-host failure", - suite: "Docker Config - Update Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "update-host failure", - suite: "Docker Config - Update Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "400 Error with error details", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("GET /docker-config/hosts", () => { - it("should retrieve list of hosts", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const hosts: DockerHost[] = [ - { id: 1, name: "H1", hostAddress: "a", secure: false }, - ]; - - mockDb.getDockerHosts.mockImplementation(() => hosts as never[]); - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/hosts"); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toEqual(hosts); - - recordTestResult({ - name: "get-hosts success", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "get-hosts success", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with hosts array", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle error when retrieval fails", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - mockDb.getDockerHosts.mockImplementationOnce(() => { - throw new Error("Fetch error"); - }); - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/hosts"); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - error: expect.any(String), - }); - - recordTestResult({ - name: "get-hosts failure", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "get-hosts failure", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "400 Error with error details", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("DELETE /docker-config/hosts/:id", () => { - it("should delete a host successfully", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const id = 5; - - try { - const app = createApp(); - const req = new Request(`http://localhost/docker-config/hosts/${id}`, { - method: "DELETE", - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - message: `Deleted docker host (${id})`, - }); - expect(mockDb.deleteDockerHost).toHaveBeenCalledWith(id); - - recordTestResult({ - name: "delete-host success", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "delete-host success", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with deletion confirmation", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle error when delete fails", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const id = 6; - - mockDb.deleteDockerHost.mockImplementationOnce(() => { - throw new Error("Delete error"); - }); - - try { - const app = createApp(); - const req = new Request(`http://localhost/docker-config/hosts/${id}`, { - method: "DELETE", - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - error: expect.any(String), - }); - - recordTestResult({ - name: "delete-host failure", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "delete-host failure", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "400 Error with error details", - received: context?.response, - }, - }); - throw error; - } - }); - }); + beforeEach(() => { + mockDb.addDockerHost.mockClear(); + mockDb.updateDockerHost.mockClear(); + mockDb.getDockerHosts.mockClear(); + mockDb.deleteDockerHost.mockClear(); + }); + + describe("POST /docker-config/add-host", () => { + it("should add a docker host successfully", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + name: "Host1", + hostAddress: "127.0.0.1:2375", + secure: false, + }; + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/add-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + message: `Added docker host (${host.name})`, + }); + expect(mockDb.addDockerHost).toHaveBeenCalledWith(host); + + recordTestResult({ + name: "add-host success", + suite: "Docker Config - Add Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "add-host success", + suite: "Docker Config - Add Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with success message", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when adding a docker host fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + name: "Host2", + hostAddress: "invalid", + secure: true, + }; + + // Set mock implementation + mockDb.addDockerHost.mockImplementationOnce(() => { + throw new Error("DB error"); + }); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/add-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + error: expect.any(String), + }); + + recordTestResult({ + name: "add-host failure", + suite: "Docker Config - Add Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "add-host failure", + suite: "Docker Config - Add Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error structure", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("POST /docker-config/update-host", () => { + it("should update a docker host successfully", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + id: 1, + name: "Host1-upd", + hostAddress: "127.0.0.1:2376", + secure: true, + }; + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/update-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + message: `Updated docker host (${host.id})`, + }); + expect(mockDb.updateDockerHost).toHaveBeenCalledWith(host); + + recordTestResult({ + name: "update-host success", + suite: "Docker Config - Update Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "update-host success", + suite: "Docker Config - Update Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with update confirmation", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when update fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + id: 2, + name: "Host2", + hostAddress: "x", + secure: false, + }; + + mockDb.updateDockerHost.mockImplementationOnce(() => { + throw new Error("Update error"); + }); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/update-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + error: expect.any(String), + }); + + recordTestResult({ + name: "update-host failure", + suite: "Docker Config - Update Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "update-host failure", + suite: "Docker Config - Update Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error details", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("GET /docker-config/hosts", () => { + it("should retrieve list of hosts", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const hosts: DockerHost[] = [ + { id: 1, name: "H1", hostAddress: "a", secure: false }, + ]; + + mockDb.getDockerHosts.mockImplementation(() => hosts as never[]); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/hosts"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toEqual(hosts); + + recordTestResult({ + name: "get-hosts success", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "get-hosts success", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with hosts array", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when retrieval fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + mockDb.getDockerHosts.mockImplementationOnce(() => { + throw new Error("Fetch error"); + }); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/hosts"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + error: expect.any(String), + }); + + recordTestResult({ + name: "get-hosts failure", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "get-hosts failure", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error details", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("DELETE /docker-config/hosts/:id", () => { + it("should delete a host successfully", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const id = 5; + + try { + const app = createApp(); + const req = new Request(`http://localhost/docker-config/hosts/${id}`, { + method: "DELETE", + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + message: `Deleted docker host (${id})`, + }); + expect(mockDb.deleteDockerHost).toHaveBeenCalledWith(id); + + recordTestResult({ + name: "delete-host success", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "delete-host success", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with deletion confirmation", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when delete fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const id = 6; + + mockDb.deleteDockerHost.mockImplementationOnce(() => { + throw new Error("Delete error"); + }); + + try { + const app = createApp(); + const req = new Request(`http://localhost/docker-config/hosts/${id}`, { + method: "DELETE", + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + error: expect.any(String), + }); + + recordTestResult({ + name: "delete-host failure", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "delete-host failure", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error details", + received: context?.response, + }, + }); + throw error; + } + }); + }); }); afterAll(() => { - generateJunitReport(); + generateMarkdownReport(); }); diff --git a/src/tests/junit-exporter.ts b/src/tests/junit-exporter.ts deleted file mode 100644 index 0ff4bd9..0000000 --- a/src/tests/junit-exporter.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { mkdirSync, writeFileSync } from "node:fs"; -import { format } from "date-fns"; -import { logger } from "~/core/utils/logger"; - -export type TestContext = { - request: { - method: string; - url: string; - headers: Record; - query?: Record; - body?: unknown; - }; - response: { - status: number; - headers: Record; - body?: unknown; - }; -}; - -type ErrorDetails = { - expected?: unknown; - received?: unknown; -}; - -type TestResult = { - name: string; - suite: string; - time: number; - error?: Error; - context?: TestContext; - errorDetails?: ErrorDetails; -}; - -export function recordTestResult(result: TestResult) { - logger.debug(`__UT__ Recording test result: ${JSON.stringify(result)}`); - testResults.push(result); -} - -export let testResults: TestResult[] = []; - -function formatContext( - context?: TestContext, - errorDetails?: ErrorDetails -): string { - if (!context) return ""; - - let output = "=== REQUEST ===\n"; - output += `Method: ${context.request.method}\n`; - output += `URL: ${context.request.url}\n`; - - if (context.request.query) { - output += `Query Params: ${JSON.stringify( - context.request.query, - null, - 2 - )}\n`; - } - - output += `Headers: ${JSON.stringify(context.request.headers, null, 2)}\n`; - - if (context.request.body) { - output += `Body: ${JSON.stringify(context.request.body, null, 2)}\n`; - } - - output += "\n=== RESPONSE ===\n"; - output += `Status: ${context.response.status}\n`; - output += `Headers: ${JSON.stringify(context.response.headers, null, 2)}\n`; - - if (context.response.body) { - output += `Body: ${JSON.stringify(context.response.body, null, 2)}\n`; - } - - if (errorDetails) { - output += "\n=== ERROR DETAILS ===\n"; - output += `Expected: ${JSON.stringify(errorDetails.expected, null, 2)}\n`; - output += `Received: ${JSON.stringify(errorDetails.received, null, 2)}\n`; - } - - return output.replace(/]]>/g, "]]]]>>"); -} - -export function generateJunitReport() { - if (testResults.length === 0) { - logger.warn("No test results to generate JUnit report."); - return; - } - - const totalTests = testResults.length; - const totalErrors = testResults.filter((r) => r.error).length; - - const testSuites = testResults.reduce((suites, result) => { - if (!suites[result.suite]) { - suites[result.suite] = []; - } - suites[result.suite].push(result); - return suites; - }, {} as Record<string, TestResult[]>); - - const xml = `<?xml version="1.0" encoding="UTF-8"?> -<testsuites tests="${totalTests}" errors="${totalErrors}"> - ${Object.entries(testSuites) - .map(([suiteName, cases]) => { - const suiteErrors = cases.filter((c) => c.error).length; - return ` - <testsuite name="${suiteName}" - tests="${cases.length}" - errors="${suiteErrors}" - timestamp="${format(new Date(), "yyyy-MM-dd'T'HH:mm:ss")}"> - ${cases - .map( - (testCase) => ` - <testcase name="${testCase.name}" classname="${suiteName}" time="${ - testCase.time - }"> - ${ - testCase.error - ? ` - <failure message="${testCase.error.message.replace(/"/g, "&quot;")}"> - <![CDATA[${testCase.error.stack?.replace(//g, "]]]]>>")} - ` - : "" - } - - ${formatContext(testCase.context)} - - ` - ) - .join("\n")} - `; - }) - .join("\n")} -`; - - mkdirSync("reports/junit", { recursive: true }); - writeFileSync( - `reports/junit/junit-${format(new Date(), "yyyy-MM-dd")}.xml`, - xml, - "utf8" - ); - - // Clear results after reporting - // resetTestResults(); - - logger.debug(`__UT__ Final data: ${JSON.stringify(testResults)}`); -} diff --git a/src/tests/markdown-exporter.ts b/src/tests/markdown-exporter.ts new file mode 100644 index 0000000..3ebda41 --- /dev/null +++ b/src/tests/markdown-exporter.ts @@ -0,0 +1,141 @@ +import { mkdirSync, writeFileSync } from "node:fs"; +import { format } from "date-fns"; +import { logger } from "~/core/utils/logger"; + +export type TestContext = { + request: { + method: string; + url: string; + headers: Record; + query?: Record; + body?: unknown; + }; + response: { + status: number; + headers: Record; + body?: unknown; + }; +}; + +type ErrorDetails = { + expected?: unknown; + received?: unknown; +}; + +type TestResult = { + name: string; + suite: string; + time: number; + error?: Error; + context?: TestContext; + errorDetails?: ErrorDetails; +}; + +export function recordTestResult(result: TestResult) { + logger.debug(`__UT__ Recording test result: ${JSON.stringify(result)}`); + testResults.push(result); +} + +export let testResults: TestResult[] = []; + +function formatContextMarkdown( + context?: TestContext, + errorDetails?: ErrorDetails +): string { + if (!context) return ""; + + let md = "```\n"; + md += `=== REQUEST ===\n`; + md += `Method: ${context.request.method}\n`; + md += `URL: ${context.request.url}\n`; + if (context.request.query) { + md += `Query Params: ${JSON.stringify(context.request.query, null, 2)}\n`; + } + md += `Headers: ${JSON.stringify(context.request.headers, null, 2)}\n`; + if (context.request.body) { + md += `Body: ${JSON.stringify(context.request.body, null, 2)}\n`; + } + md += `\n=== RESPONSE ===\n`; + md += `Status: ${context.response.status}\n`; + md += `Headers: ${JSON.stringify(context.response.headers, null, 2)}\n`; + if (context.response.body) { + md += `Body: ${JSON.stringify(context.response.body, null, 2)}\n`; + } + if (errorDetails) { + md += `\n=== ERROR DETAILS ===\n`; + md += `Expected: ${JSON.stringify(errorDetails.expected, null, 2)}\n`; + md += `Received: ${JSON.stringify(errorDetails.received, null, 2)}\n`; + } + md += "```\n"; + return md; +} + +export function generateMarkdownReport() { + if (testResults.length === 0) { + logger.warn("No test results to generate markdown report."); + return; + } + + const totalTests = testResults.length; + const totalErrors = testResults.filter((r) => r.error).length; + + const testSuites = testResults.reduce((suites, result) => { + if (!suites[result.suite]) { + suites[result.suite] = []; + } + suites[result.suite].push(result); + return suites; + }, {} as Record); + + let md = `# Test Report - ${format(new Date(), "yyyy-MM-dd")}\n`; + md += `\n**Total Tests:** ${totalTests} +`; + md += `**Total Failures:** ${totalErrors}\n`; + + for (const [suiteName, cases] of Object.entries(testSuites)) { + const suiteErrors = cases.filter((c) => c.error).length; + md += `\n## Suite: ${suiteName} +`; + md += `- Tests: ${cases.length} +`; + md += `- Failures: ${suiteErrors}\n`; + + for (const test of cases) { + const status = test.error ? "❌ Failed" : "✅ Passed"; + md += `\n### ${test.name} (${(test.time / 1000).toFixed(2)}s) +`; + md += `- Status: **${status}** \n`; + + if (test.error) { + const msg = test.error.message + .replace(//g, ">"); + const stack = test.error.stack + ?.replace(//g, ">"); + md += `\n
\nError Details\n\n`; + md += `**Message:** ${msg} \n`; + if (stack) { + md += `\n\`\`\`\n${stack}\n\`\`\`\n`; + } + md += `
\n`; + } + + if (test.context) { + md += `\n
\nRequest/Response Context\n\n`; + md += formatContextMarkdown(test.context, test.errorDetails); + md += `
\n`; + } + } + } + + // Ensure directory exists + mkdirSync("reports/markdown", { recursive: true }); + const filename = `reports/markdown/test-report-${format( + new Date(), + "yyyy-MM-dd" + )}.md`; + writeFileSync(filename, md, "utf8"); + + logger.debug(`__UT__ Markdown report written to ${filename}`); +} From 9d73c7dbffe18ce778b58cbda7c6e4fa78e7be80 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 14 May 2025 17:28:07 +0200 Subject: [PATCH 318/324] CI/CD: Fix my wrong string flavour :sob: --- .github/workflows/ci.yml | 2 +- src/tests/api-config.spec.ts | 634 +++++++++++----------- src/tests/docker-manager.spec.ts | 866 +++++++++++++++---------------- src/tests/markdown-exporter.ts | 227 ++++---- 4 files changed, 866 insertions(+), 863 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5e687e4..42a7875 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,7 +41,7 @@ jobs: run: | SUMMARY="" for element in $(ls reports/markdown); do - SUMMARY="$(echo -e "${SUMMARY}\n$(cat "${element}")")" + SUMMARY="$(echo -e "${SUMMARY}\n$(cat "reports/markdown${element}")")" done cho "$SUMMARY" >> $GITHUB_STEP_SUMMARY build-scan: diff --git a/src/tests/api-config.spec.ts b/src/tests/api-config.spec.ts index eb62cc8..ba3e7b3 100644 --- a/src/tests/api-config.spec.ts +++ b/src/tests/api-config.spec.ts @@ -6,339 +6,339 @@ import { generateMarkdownReport, recordTestResult } from "./markdown-exporter"; import type { TestContext } from "./markdown-exporter"; const mockDb = { - updateConfig: mock(() => ({})), - backupDatabase: mock( - () => `dockstatapi-${new Date().toISOString().slice(0, 10)}.db.bak` - ), - restoreDatabase: mock(), - findLatestBackup: mock(() => "dockstatapi-2025-05-06.db.bak"), + updateConfig: mock(() => ({})), + backupDatabase: mock( + () => `dockstatapi-${new Date().toISOString().slice(0, 10)}.db.bak`, + ), + restoreDatabase: mock(), + findLatestBackup: mock(() => "dockstatapi-2025-05-06.db.bak"), }; mock.module("node:fs", () => ({ - existsSync: mock((path) => path.includes("dockstatapi")), - readdirSync: mock(() => [ - "dockstatapi-2025-05-06.db.bak", - "dockstatapi.db", - "dockstatapi.db-shm", - ]), - unlinkSync: mock(), + existsSync: mock((path) => path.includes("dockstatapi")), + readdirSync: mock(() => [ + "dockstatapi-2025-05-06.db.bak", + "dockstatapi.db", + "dockstatapi.db-shm", + ]), + unlinkSync: mock(), })); const mockPlugins = [ - { - name: "docker-monitor", - version: "1.2.0", - status: "active", - }, + { + name: "docker-monitor", + version: "1.2.0", + status: "active", + }, ]; const createTestApp = () => - new Elysia().use(apiConfigRoutes).decorate({ - dbFunctions: mockDb, - pluginManager: { - getLoadedPlugins: mock(() => mockPlugins), - getPlugin: mock((name) => mockPlugins.find((p) => p.name === name)), - }, - logger: { - ...logger, - debug: mock(), - error: mock(), - info: mock(), - }, - }); + new Elysia().use(apiConfigRoutes).decorate({ + dbFunctions: mockDb, + pluginManager: { + getLoadedPlugins: mock(() => mockPlugins), + getPlugin: mock((name) => mockPlugins.find((p) => p.name === name)), + }, + logger: { + ...logger, + debug: mock(), + error: mock(), + info: mock(), + }, + }); async function captureTestContext( - req: Request, - res: Response + req: Request, + res: Response, ): Promise { - const responseStatus = res.status; - const responseHeaders = Object.fromEntries(res.headers.entries()); - let responseBody: string; - - try { - responseBody = await res.clone().json(); - } catch (parseError) { - try { - responseBody = await res.clone().text(); - } catch (textError) { - responseBody = "Unparseable response content"; - } - } - - return { - request: { - method: req.method, - url: req.url, - headers: Object.fromEntries(req.headers.entries()), - body: req.body ? await req.clone().text() : undefined, - }, - response: { - status: responseStatus, - headers: responseHeaders, - body: responseBody, - }, - }; + const responseStatus = res.status; + const responseHeaders = Object.fromEntries(res.headers.entries()); + let responseBody: string; + + try { + responseBody = await res.clone().json(); + } catch (parseError) { + try { + responseBody = await res.clone().text(); + } catch (textError) { + responseBody = "Unparseable response content"; + } + } + + return { + request: { + method: req.method, + url: req.url, + headers: Object.fromEntries(req.headers.entries()), + body: req.body ? await req.clone().text() : undefined, + }, + response: { + status: responseStatus, + headers: responseHeaders, + body: responseBody, + }, + }; } describe("API Configuration Endpoints", () => { - beforeEach(() => { - mockDb.updateConfig.mockClear(); - }); - - describe("Core Configuration", () => { - it("should retrieve current config with hashed API key", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - try { - const app = createTestApp(); - const req = new Request("http://localhost:3000/config"); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - fetching_interval: expect.any(Number), - keep_data_for: expect.any(Number), - }); - - recordTestResult({ - name: "should retrieve current config with hashed API key", - suite: "API Configuration Endpoints - Core Configuration", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "should retrieve current config with hashed API key", - suite: "API Configuration Endpoints - Core Configuration", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with valid config structure", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle config update with valid payload", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - try { - const app = createTestApp(); - const requestBody = { - fetching_interval: 15, - keep_data_for: 30, - api_key: "new-valid-key", - }; - const req = new Request("http://localhost:3000/config/update", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(requestBody), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - success: true, - message: expect.stringContaining("Updated"), - }); - - recordTestResult({ - name: "should handle config update with valid payload", - suite: "API Configuration Endpoints - Core Configuration", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "should handle config update with valid payload", - suite: "API Configuration Endpoints - Core Configuration", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with update confirmation", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("Plugin Management", () => { - it("should list active plugins with metadata", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - try { - const app = createTestApp(); - const req = new Request("http://localhost:3000/config/plugins"); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toEqual( - [] - //expect.arrayContaining([ - // expect.objectContaining({ - // name: expect.any(String), - // version: expect.any(String), - // status: expect.any(String), - // }), - //]) - ); - - recordTestResult({ - name: "should list active plugins with metadata", - suite: "API Configuration Endpoints - Plugin Management", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "should list active plugins with metadata", - suite: "API Configuration Endpoints - Plugin Management", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with plugin list", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("Backup Management", () => { - it("should generate timestamped backup files", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - try { - const app = createTestApp(); - const req = new Request("http://localhost:3000/config/backup", { - method: "POST", - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - const { message } = context.response.body as { message: string }; - expect(message).toMatch( - /^data\/dockstatapi-\d{2}-\d{2}-\d{4}-1\.db\.bak$/ - ); - - recordTestResult({ - name: "should generate timestamped backup files", - suite: "API Configuration Endpoints - Backup Management", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "should generate timestamped backup files", - suite: "API Configuration Endpoints - Backup Management", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with backup path", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should list valid backup files", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - try { - const app = createTestApp(); - const req = new Request("http://localhost:3000/config/backup"); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - const backups = context.response.body as string[]; - expect(backups).toEqual( - expect.arrayContaining([expect.stringMatching(/\.db\.bak$/)]) - ); - - recordTestResult({ - name: "should list valid backup files", - suite: "API Configuration Endpoints - Backup Management", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "should list valid backup files", - suite: "API Configuration Endpoints - Backup Management", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with backup list", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("Error Handling", () => { - it("should return proper error format", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - try { - const app = createTestApp(); - const req = new Request("http://localhost:3000/random_link", { - method: "GET", - headers: { "Content-Type": "application/json" }, - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(404); - - recordTestResult({ - name: "should return proper error format", - suite: - "API Configuration Endpoints - Error Handling of unkown routes", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "should return proper error format", - suite: "API Configuration Endpoints - Error Handling", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "500 Error with structured error format", - received: context?.response, - }, - }); - throw error; - } - }); - }); + beforeEach(() => { + mockDb.updateConfig.mockClear(); + }); + + describe("Core Configuration", () => { + it("should retrieve current config with hashed API key", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const req = new Request("http://localhost:3000/config"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + fetching_interval: expect.any(Number), + keep_data_for: expect.any(Number), + }); + + recordTestResult({ + name: "should retrieve current config with hashed API key", + suite: "API Configuration Endpoints - Core Configuration", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should retrieve current config with hashed API key", + suite: "API Configuration Endpoints - Core Configuration", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with valid config structure", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle config update with valid payload", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const requestBody = { + fetching_interval: 15, + keep_data_for: 30, + api_key: "new-valid-key", + }; + const req = new Request("http://localhost:3000/config/update", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(requestBody), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + success: true, + message: expect.stringContaining("Updated"), + }); + + recordTestResult({ + name: "should handle config update with valid payload", + suite: "API Configuration Endpoints - Core Configuration", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should handle config update with valid payload", + suite: "API Configuration Endpoints - Core Configuration", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with update confirmation", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("Plugin Management", () => { + it("should list active plugins with metadata", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const req = new Request("http://localhost:3000/config/plugins"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toEqual( + [], + //expect.arrayContaining([ + // expect.objectContaining({ + // name: expect.any(String), + // version: expect.any(String), + // status: expect.any(String), + // }), + //]) + ); + + recordTestResult({ + name: "should list active plugins with metadata", + suite: "API Configuration Endpoints - Plugin Management", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should list active plugins with metadata", + suite: "API Configuration Endpoints - Plugin Management", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with plugin list", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("Backup Management", () => { + it("should generate timestamped backup files", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const req = new Request("http://localhost:3000/config/backup", { + method: "POST", + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + const { message } = context.response.body as { message: string }; + expect(message).toMatch( + /^data\/dockstatapi-\d{2}-\d{2}-\d{4}-1\.db\.bak$/, + ); + + recordTestResult({ + name: "should generate timestamped backup files", + suite: "API Configuration Endpoints - Backup Management", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should generate timestamped backup files", + suite: "API Configuration Endpoints - Backup Management", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with backup path", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should list valid backup files", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const req = new Request("http://localhost:3000/config/backup"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + const backups = context.response.body as string[]; + expect(backups).toEqual( + expect.arrayContaining([expect.stringMatching(/\.db\.bak$/)]), + ); + + recordTestResult({ + name: "should list valid backup files", + suite: "API Configuration Endpoints - Backup Management", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should list valid backup files", + suite: "API Configuration Endpoints - Backup Management", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with backup list", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("Error Handling", () => { + it("should return proper error format", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const req = new Request("http://localhost:3000/random_link", { + method: "GET", + headers: { "Content-Type": "application/json" }, + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(404); + + recordTestResult({ + name: "should return proper error format", + suite: + "API Configuration Endpoints - Error Handling of unkown routes", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should return proper error format", + suite: "API Configuration Endpoints - Error Handling", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "500 Error with structured error format", + received: context?.response, + }, + }); + throw error; + } + }); + }); }); afterAll(() => { - generateMarkdownReport(); + generateMarkdownReport(); }); diff --git a/src/tests/docker-manager.spec.ts b/src/tests/docker-manager.spec.ts index 2d1e6ec..df9d65d 100644 --- a/src/tests/docker-manager.spec.ts +++ b/src/tests/docker-manager.spec.ts @@ -3,462 +3,462 @@ import { Elysia } from "elysia"; import { dbFunctions } from "~/core/database"; import { dockerRoutes } from "~/routes/docker-manager"; import { - generateMarkdownReport, - recordTestResult, - testResults, + generateMarkdownReport, + recordTestResult, + testResults, } from "./markdown-exporter"; import type { TestContext } from "./markdown-exporter"; type DockerHost = { - id?: number; - name: string; - hostAddress: string; - secure: boolean; + id?: number; + name: string; + hostAddress: string; + secure: boolean; }; const mockDb = { - addDockerHost: mock(() => ({ - changes: 1, - lastInsertRowid: 1, - })), - updateDockerHost: mock(() => ({ - changes: 1, - lastInsertRowid: 1, - })), - getDockerHosts: mock(() => []), - deleteDockerHost: mock(() => ({ - changes: 1, - lastInsertRowid: 1, - })), + addDockerHost: mock(() => ({ + changes: 1, + lastInsertRowid: 1, + })), + updateDockerHost: mock(() => ({ + changes: 1, + lastInsertRowid: 1, + })), + getDockerHosts: mock(() => []), + deleteDockerHost: mock(() => ({ + changes: 1, + lastInsertRowid: 1, + })), }; mock.module("~/core/database", () => ({ - dbFunctions: mockDb, + dbFunctions: mockDb, })); mock.module("~/core/utils/logger", () => ({ - logger: { - debug: mock(), - info: mock(), - error: mock(), - }, + logger: { + debug: mock(), + info: mock(), + error: mock(), + }, })); const createApp = () => new Elysia().use(dockerRoutes).decorate({}); async function captureTestContext( - req: Request, - res: Response + req: Request, + res: Response, ): Promise { - const responseStatus = res.status; - const responseHeaders = Object.fromEntries(res.headers.entries()); - let responseBody: unknown; - - try { - responseBody = await res.clone().json(); - } catch (parseError) { - try { - responseBody = await res.clone().text(); - } catch { - responseBody = "Unparseable response content"; - } - } - - return { - request: { - method: req.method, - url: req.url, - headers: Object.fromEntries(req.headers.entries()), - body: req.body ? await req.clone().text() : undefined, - }, - response: { - status: responseStatus, - headers: responseHeaders, - body: responseBody, - }, - }; + const responseStatus = res.status; + const responseHeaders = Object.fromEntries(res.headers.entries()); + let responseBody: unknown; + + try { + responseBody = await res.clone().json(); + } catch (parseError) { + try { + responseBody = await res.clone().text(); + } catch { + responseBody = "Unparseable response content"; + } + } + + return { + request: { + method: req.method, + url: req.url, + headers: Object.fromEntries(req.headers.entries()), + body: req.body ? await req.clone().text() : undefined, + }, + response: { + status: responseStatus, + headers: responseHeaders, + body: responseBody, + }, + }; } describe("Docker Configuration Endpoints", () => { - beforeEach(() => { - mockDb.addDockerHost.mockClear(); - mockDb.updateDockerHost.mockClear(); - mockDb.getDockerHosts.mockClear(); - mockDb.deleteDockerHost.mockClear(); - }); - - describe("POST /docker-config/add-host", () => { - it("should add a docker host successfully", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const host: DockerHost = { - name: "Host1", - hostAddress: "127.0.0.1:2375", - secure: false, - }; - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/add-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - message: `Added docker host (${host.name})`, - }); - expect(mockDb.addDockerHost).toHaveBeenCalledWith(host); - - recordTestResult({ - name: "add-host success", - suite: "Docker Config - Add Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "add-host success", - suite: "Docker Config - Add Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with success message", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle error when adding a docker host fails", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const host: DockerHost = { - name: "Host2", - hostAddress: "invalid", - secure: true, - }; - - // Set mock implementation - mockDb.addDockerHost.mockImplementationOnce(() => { - throw new Error("DB error"); - }); - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/add-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - error: expect.any(String), - }); - - recordTestResult({ - name: "add-host failure", - suite: "Docker Config - Add Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "add-host failure", - suite: "Docker Config - Add Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "400 Error with error structure", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("POST /docker-config/update-host", () => { - it("should update a docker host successfully", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const host: DockerHost = { - id: 1, - name: "Host1-upd", - hostAddress: "127.0.0.1:2376", - secure: true, - }; - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/update-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - message: `Updated docker host (${host.id})`, - }); - expect(mockDb.updateDockerHost).toHaveBeenCalledWith(host); - - recordTestResult({ - name: "update-host success", - suite: "Docker Config - Update Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "update-host success", - suite: "Docker Config - Update Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with update confirmation", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle error when update fails", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const host: DockerHost = { - id: 2, - name: "Host2", - hostAddress: "x", - secure: false, - }; - - mockDb.updateDockerHost.mockImplementationOnce(() => { - throw new Error("Update error"); - }); - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/update-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - error: expect.any(String), - }); - - recordTestResult({ - name: "update-host failure", - suite: "Docker Config - Update Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "update-host failure", - suite: "Docker Config - Update Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "400 Error with error details", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("GET /docker-config/hosts", () => { - it("should retrieve list of hosts", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const hosts: DockerHost[] = [ - { id: 1, name: "H1", hostAddress: "a", secure: false }, - ]; - - mockDb.getDockerHosts.mockImplementation(() => hosts as never[]); - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/hosts"); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toEqual(hosts); - - recordTestResult({ - name: "get-hosts success", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "get-hosts success", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with hosts array", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle error when retrieval fails", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - mockDb.getDockerHosts.mockImplementationOnce(() => { - throw new Error("Fetch error"); - }); - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/hosts"); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - error: expect.any(String), - }); - - recordTestResult({ - name: "get-hosts failure", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "get-hosts failure", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "400 Error with error details", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("DELETE /docker-config/hosts/:id", () => { - it("should delete a host successfully", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const id = 5; - - try { - const app = createApp(); - const req = new Request(`http://localhost/docker-config/hosts/${id}`, { - method: "DELETE", - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - message: `Deleted docker host (${id})`, - }); - expect(mockDb.deleteDockerHost).toHaveBeenCalledWith(id); - - recordTestResult({ - name: "delete-host success", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "delete-host success", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with deletion confirmation", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle error when delete fails", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const id = 6; - - mockDb.deleteDockerHost.mockImplementationOnce(() => { - throw new Error("Delete error"); - }); - - try { - const app = createApp(); - const req = new Request(`http://localhost/docker-config/hosts/${id}`, { - method: "DELETE", - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - error: expect.any(String), - }); - - recordTestResult({ - name: "delete-host failure", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "delete-host failure", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "400 Error with error details", - received: context?.response, - }, - }); - throw error; - } - }); - }); + beforeEach(() => { + mockDb.addDockerHost.mockClear(); + mockDb.updateDockerHost.mockClear(); + mockDb.getDockerHosts.mockClear(); + mockDb.deleteDockerHost.mockClear(); + }); + + describe("POST /docker-config/add-host", () => { + it("should add a docker host successfully", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + name: "Host1", + hostAddress: "127.0.0.1:2375", + secure: false, + }; + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/add-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + message: `Added docker host (${host.name})`, + }); + expect(mockDb.addDockerHost).toHaveBeenCalledWith(host); + + recordTestResult({ + name: "add-host success", + suite: "Docker Config - Add Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "add-host success", + suite: "Docker Config - Add Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with success message", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when adding a docker host fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + name: "Host2", + hostAddress: "invalid", + secure: true, + }; + + // Set mock implementation + mockDb.addDockerHost.mockImplementationOnce(() => { + throw new Error("DB error"); + }); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/add-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + error: expect.any(String), + }); + + recordTestResult({ + name: "add-host failure", + suite: "Docker Config - Add Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "add-host failure", + suite: "Docker Config - Add Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error structure", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("POST /docker-config/update-host", () => { + it("should update a docker host successfully", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + id: 1, + name: "Host1-upd", + hostAddress: "127.0.0.1:2376", + secure: true, + }; + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/update-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + message: `Updated docker host (${host.id})`, + }); + expect(mockDb.updateDockerHost).toHaveBeenCalledWith(host); + + recordTestResult({ + name: "update-host success", + suite: "Docker Config - Update Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "update-host success", + suite: "Docker Config - Update Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with update confirmation", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when update fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + id: 2, + name: "Host2", + hostAddress: "x", + secure: false, + }; + + mockDb.updateDockerHost.mockImplementationOnce(() => { + throw new Error("Update error"); + }); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/update-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + error: expect.any(String), + }); + + recordTestResult({ + name: "update-host failure", + suite: "Docker Config - Update Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "update-host failure", + suite: "Docker Config - Update Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error details", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("GET /docker-config/hosts", () => { + it("should retrieve list of hosts", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const hosts: DockerHost[] = [ + { id: 1, name: "H1", hostAddress: "a", secure: false }, + ]; + + mockDb.getDockerHosts.mockImplementation(() => hosts as never[]); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/hosts"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toEqual(hosts); + + recordTestResult({ + name: "get-hosts success", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "get-hosts success", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with hosts array", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when retrieval fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + mockDb.getDockerHosts.mockImplementationOnce(() => { + throw new Error("Fetch error"); + }); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/hosts"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + error: expect.any(String), + }); + + recordTestResult({ + name: "get-hosts failure", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "get-hosts failure", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error details", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("DELETE /docker-config/hosts/:id", () => { + it("should delete a host successfully", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const id = 5; + + try { + const app = createApp(); + const req = new Request(`http://localhost/docker-config/hosts/${id}`, { + method: "DELETE", + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + message: `Deleted docker host (${id})`, + }); + expect(mockDb.deleteDockerHost).toHaveBeenCalledWith(id); + + recordTestResult({ + name: "delete-host success", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "delete-host success", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with deletion confirmation", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when delete fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const id = 6; + + mockDb.deleteDockerHost.mockImplementationOnce(() => { + throw new Error("Delete error"); + }); + + try { + const app = createApp(); + const req = new Request(`http://localhost/docker-config/hosts/${id}`, { + method: "DELETE", + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + error: expect.any(String), + }); + + recordTestResult({ + name: "delete-host failure", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "delete-host failure", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error details", + received: context?.response, + }, + }); + throw error; + } + }); + }); }); afterAll(() => { - generateMarkdownReport(); + generateMarkdownReport(); }); diff --git a/src/tests/markdown-exporter.ts b/src/tests/markdown-exporter.ts index 3ebda41..2d55b48 100644 --- a/src/tests/markdown-exporter.ts +++ b/src/tests/markdown-exporter.ts @@ -3,139 +3,142 @@ import { format } from "date-fns"; import { logger } from "~/core/utils/logger"; export type TestContext = { - request: { - method: string; - url: string; - headers: Record; - query?: Record; - body?: unknown; - }; - response: { - status: number; - headers: Record; - body?: unknown; - }; + request: { + method: string; + url: string; + headers: Record; + query?: Record; + body?: unknown; + }; + response: { + status: number; + headers: Record; + body?: unknown; + }; }; type ErrorDetails = { - expected?: unknown; - received?: unknown; + expected?: unknown; + received?: unknown; }; type TestResult = { - name: string; - suite: string; - time: number; - error?: Error; - context?: TestContext; - errorDetails?: ErrorDetails; + name: string; + suite: string; + time: number; + error?: Error; + context?: TestContext; + errorDetails?: ErrorDetails; }; export function recordTestResult(result: TestResult) { - logger.debug(`__UT__ Recording test result: ${JSON.stringify(result)}`); - testResults.push(result); + logger.debug(`__UT__ Recording test result: ${JSON.stringify(result)}`); + testResults.push(result); } -export let testResults: TestResult[] = []; +export const testResults: TestResult[] = []; function formatContextMarkdown( - context?: TestContext, - errorDetails?: ErrorDetails + context?: TestContext, + errorDetails?: ErrorDetails, ): string { - if (!context) return ""; - - let md = "```\n"; - md += `=== REQUEST ===\n`; - md += `Method: ${context.request.method}\n`; - md += `URL: ${context.request.url}\n`; - if (context.request.query) { - md += `Query Params: ${JSON.stringify(context.request.query, null, 2)}\n`; - } - md += `Headers: ${JSON.stringify(context.request.headers, null, 2)}\n`; - if (context.request.body) { - md += `Body: ${JSON.stringify(context.request.body, null, 2)}\n`; - } - md += `\n=== RESPONSE ===\n`; - md += `Status: ${context.response.status}\n`; - md += `Headers: ${JSON.stringify(context.response.headers, null, 2)}\n`; - if (context.response.body) { - md += `Body: ${JSON.stringify(context.response.body, null, 2)}\n`; - } - if (errorDetails) { - md += `\n=== ERROR DETAILS ===\n`; - md += `Expected: ${JSON.stringify(errorDetails.expected, null, 2)}\n`; - md += `Received: ${JSON.stringify(errorDetails.received, null, 2)}\n`; - } - md += "```\n"; - return md; + if (!context) return ""; + + let md = "```\n"; + md += "=== REQUEST ===\n"; + md += `Method: ${context.request.method}\n`; + md += `URL: ${context.request.url}\n`; + if (context.request.query) { + md += `Query Params: ${JSON.stringify(context.request.query, null, 2)}\n`; + } + md += `Headers: ${JSON.stringify(context.request.headers, null, 2)}\n`; + if (context.request.body) { + md += `Body: ${JSON.stringify(context.request.body, null, 2)}\n`; + } + md += "\n=== RESPONSE ===\n"; + md += `Status: ${context.response.status}\n`; + md += `Headers: ${JSON.stringify(context.response.headers, null, 2)}\n`; + if (context.response.body) { + md += `Body: ${JSON.stringify(context.response.body, null, 2)}\n`; + } + if (errorDetails) { + md += "\n=== ERROR DETAILS ===\n"; + md += `Expected: ${JSON.stringify(errorDetails.expected, null, 2)}\n`; + md += `Received: ${JSON.stringify(errorDetails.received, null, 2)}\n`; + } + md += "```\n"; + return md; } export function generateMarkdownReport() { - if (testResults.length === 0) { - logger.warn("No test results to generate markdown report."); - return; - } - - const totalTests = testResults.length; - const totalErrors = testResults.filter((r) => r.error).length; - - const testSuites = testResults.reduce((suites, result) => { - if (!suites[result.suite]) { - suites[result.suite] = []; - } - suites[result.suite].push(result); - return suites; - }, {} as Record); - - let md = `# Test Report - ${format(new Date(), "yyyy-MM-dd")}\n`; - md += `\n**Total Tests:** ${totalTests} + if (testResults.length === 0) { + logger.warn("No test results to generate markdown report."); + return; + } + + const totalTests = testResults.length; + const totalErrors = testResults.filter((r) => r.error).length; + + const testSuites = testResults.reduce( + (suites, result) => { + if (!suites[result.suite]) { + suites[result.suite] = []; + } + suites[result.suite].push(result); + return suites; + }, + {} as Record, + ); + + let md = `# Test Report - ${format(new Date(), "yyyy-MM-dd")}\n`; + md += `\n**Total Tests:** ${totalTests} `; - md += `**Total Failures:** ${totalErrors}\n`; + md += `**Total Failures:** ${totalErrors}\n`; - for (const [suiteName, cases] of Object.entries(testSuites)) { - const suiteErrors = cases.filter((c) => c.error).length; - md += `\n## Suite: ${suiteName} + for (const [suiteName, cases] of Object.entries(testSuites)) { + const suiteErrors = cases.filter((c) => c.error).length; + md += `\n## Suite: ${suiteName} `; - md += `- Tests: ${cases.length} + md += `- Tests: ${cases.length} `; - md += `- Failures: ${suiteErrors}\n`; + md += `- Failures: ${suiteErrors}\n`; - for (const test of cases) { - const status = test.error ? "❌ Failed" : "✅ Passed"; - md += `\n### ${test.name} (${(test.time / 1000).toFixed(2)}s) + for (const test of cases) { + const status = test.error ? "❌ Failed" : "✅ Passed"; + md += `\n### ${test.name} (${(test.time / 1000).toFixed(2)}s) `; - md += `- Status: **${status}** \n`; - - if (test.error) { - const msg = test.error.message - .replace(//g, ">"); - const stack = test.error.stack - ?.replace(//g, ">"); - md += `\n
\nError Details\n\n`; - md += `**Message:** ${msg} \n`; - if (stack) { - md += `\n\`\`\`\n${stack}\n\`\`\`\n`; - } - md += `
\n`; - } - - if (test.context) { - md += `\n
\nRequest/Response Context\n\n`; - md += formatContextMarkdown(test.context, test.errorDetails); - md += `
\n`; - } - } - } - - // Ensure directory exists - mkdirSync("reports/markdown", { recursive: true }); - const filename = `reports/markdown/test-report-${format( - new Date(), - "yyyy-MM-dd" - )}.md`; - writeFileSync(filename, md, "utf8"); - - logger.debug(`__UT__ Markdown report written to ${filename}`); + md += `- Status: **${status}** \n`; + + if (test.error) { + const msg = test.error.message + .replace(//g, ">"); + const stack = test.error.stack + ?.replace(//g, ">"); + md += "\n
\nError Details\n\n"; + md += `**Message:** ${msg} \n`; + if (stack) { + md += `\n\`\`\`\n${stack}\n\`\`\`\n`; + } + md += "
\n"; + } + + if (test.context) { + md += "\n
\nRequest/Response Context\n\n"; + md += formatContextMarkdown(test.context, test.errorDetails); + md += "
\n"; + } + } + } + + // Ensure directory exists + mkdirSync("reports/markdown", { recursive: true }); + const filename = `reports/markdown/test-report-${format( + new Date(), + "yyyy-MM-dd", + )}.md`; + writeFileSync(filename, md, "utf8"); + + logger.debug(`__UT__ Markdown report written to ${filename}`); } From dae4d078234d278f50d15d3243b98f44a0a03652 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 14 May 2025 17:29:27 +0200 Subject: [PATCH 319/324] CI/CD: Fix wrong path in for loop --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 42a7875..56f8e78 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,7 +41,7 @@ jobs: run: | SUMMARY="" for element in $(ls reports/markdown); do - SUMMARY="$(echo -e "${SUMMARY}\n$(cat "reports/markdown${element}")")" + SUMMARY="$(echo -e "${SUMMARY}\n$(cat "reports/markdown/${element}")")" done cho "$SUMMARY" >> $GITHUB_STEP_SUMMARY build-scan: From c8dbcbd4f19629e0d37bfecdfc5449bdbfbd7332 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 14 May 2025 17:30:51 +0200 Subject: [PATCH 320/324] CI/CD: fix type --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 56f8e78..6cf4628 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,7 @@ jobs: for element in $(ls reports/markdown); do SUMMARY="$(echo -e "${SUMMARY}\n$(cat "reports/markdown/${element}")")" done - cho "$SUMMARY" >> $GITHUB_STEP_SUMMARY + echo "$SUMMARY" >> $GITHUB_STEP_SUMMARY build-scan: name: Build and Security Scan runs-on: ubuntu-latest From 17b1f98a46b69964520be1265639883301bf675b Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 14 May 2025 17:40:26 +0200 Subject: [PATCH 321/324] CI/CD: Add CONTRIBUTORS.md workflow --- .github/workflows/contributors.yml | 21 +++++++++++++++++++++ CONTRIBUTORS.md | 0 2 files changed, 21 insertions(+) create mode 100644 .github/workflows/contributors.yml create mode 100644 CONTRIBUTORS.md diff --git a/.github/workflows/contributors.yml b/.github/workflows/contributors.yml new file mode 100644 index 0000000..9edece2 --- /dev/null +++ b/.github/workflows/contributors.yml @@ -0,0 +1,21 @@ +name: Update CONTRIBUTORS file +on: + schedule: + - cron: "0 0 1 * *" + workflow_dispatch: +jobs: + main: + runs-on: ubuntu-latest + steps: + - uses: minicli/action-contributors@v3.3 + name: "Update a projects CONTRIBUTORS file" + env: + CONTRIB_REPOSITORY: "Its4Nik/DockStatAPI" + CONTRIB_OUTPUT_FILE: "CONTRIBUTORS.md" + + - name: Commit changes + uses: test-room-7/action-update-file@v1 + with: + file-path: "CONTRIBUTORS.md" + commit-msg: Update Contributors + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 0000000..e69de29 From 9a455d961a17e861820d859f2832eaf951268c48 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Fri, 16 May 2025 10:32:54 +0200 Subject: [PATCH 322/324] Feat: Adjust error responses --- docker/docker-compose.dev.yaml | 56 +- src/core/utils/response-handler.ts | 69 +- src/routes/api-config.ts | 1114 +++++++++++++-------------- src/routes/docker-manager.ts | 504 ++++++------ src/routes/docker-stats.ts | 1135 ++++++++++++++-------------- src/tests/docker-manager.spec.ts | 866 ++++++++++----------- 6 files changed, 1863 insertions(+), 1881 deletions(-) diff --git a/docker/docker-compose.dev.yaml b/docker/docker-compose.dev.yaml index 799ae7a..f302c58 100644 --- a/docker/docker-compose.dev.yaml +++ b/docker/docker-compose.dev.yaml @@ -12,34 +12,34 @@ services: ports: - 2375:2375 environment: - - ALLOW_START=1 #optional - - ALLOW_STOP=1 #optional - - ALLOW_RESTARTS=1 #optional - - AUTH=1 #optional - - BUILD=1 #optional - - COMMIT=1 #optional - - CONFIGS=1 #optional - - CONTAINERS=1 #optional - - DISABLE_IPV6=1 #optional - - DISTRIBUTION=1 #optional - - EVENTS=1 #optional - - EXEC=1 #optional - - IMAGES=1 #optional - - INFO=1 #optional - - NETWORKS=1 #optional - - NODES=1 #optional - - PING=1 #optional - - PLUGINS=1 #optional - - POST=1 #optional - - PROXY_READ_TIMEOUT=240 #optional - - SECRETS=1 #optional - - SERVICES=1 #optional - - SESSION=1 #optional - - SWARM=1 #optional - - SYSTEM=1 #optional - - TASKS=1 #optional - - VERSION=1 #optional - - VOLUMES=1 #optional + - ALLOW_START=1 + - ALLOW_STOP=1 + - ALLOW_RESTARTS=1 + - AUTH=1 + - BUILD=1 + - COMMIT=1 + - CONFIGS=1 + - CONTAINERS=1 + - DISABLE_IPV6=1 + - DISTRIBUTION=1 + - EVENTS=1 + - EXEC=1 + - IMAGES=1 + - INFO=1 + - NETWORKS=1 + - NODES=1 + - PING=1 + - PLUGINS=1 + - POST=1 + - PROXY_READ_TIMEOUT=240 + - SECRETS=1 + - SERVICES=1 + - SESSION=1 + - SWARM=1 + - SYSTEM=1 + - TASKS=1 + - VERSION=1 + - VOLUMES=1 sqlite-web: container_name: sqlite-web diff --git a/src/core/utils/response-handler.ts b/src/core/utils/response-handler.ts index 8bfe6ec..990ae81 100644 --- a/src/core/utils/response-handler.ts +++ b/src/core/utils/response-handler.ts @@ -1,43 +1,42 @@ import { logger } from "~/core/utils/logger"; - import type { set } from "~/typings/elysiajs"; export const responseHandler = { - error( - set: set, - error: string, - response_message: string, - error_code?: number, - ) { - set.status = error_code || 500; - logger.error(`${response_message} - ${error}`); - return { error: `${response_message}` }; - }, + error( + set: set, + error: string, + response_message: string, + error_code?: number + ) { + set.status = error_code || 500; + logger.error(`${response_message} - ${error}`); + return { success: false, message: response_message, error: String(error) }; + }, - ok(set: set, response_message: string) { - set.status = 200; - logger.debug(response_message); - return { success: true, message: response_message }; - }, + ok(set: set, response_message: string) { + set.status = 200; + logger.debug(response_message); + return { success: true, message: response_message }; + }, - simple_error(set: set, response_message: string, status_code?: number) { - set.status = status_code || 502; - logger.warn(response_message); - return { error: response_message }; - }, + simple_error(set: set, response_message: string, status_code?: number) { + set.status = status_code || 502; + logger.warn(response_message); + return { success: false, message: response_message }; + }, - reject( - set: set, - reject: CallableFunction, - response_message: string, - error?: string, - ) { - set.status = 501; - if (error) { - logger.error(`${response_message} - ${error}`); - } else { - logger.error(response_message); - } - return reject(new Error(response_message)); - }, + reject( + set: set, + reject: CallableFunction, + response_message: string, + error?: string + ) { + set.status = 501; + if (error) { + logger.error(`${response_message} - ${error}`); + } else { + logger.error(response_message); + } + return reject(new Error(response_message)); + }, }; diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index 8759505..bfbaa75 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -4,583 +4,583 @@ import { dbFunctions } from "~/core/database"; import { pluginManager } from "~/core/plugins/plugin-manager"; import { logger } from "~/core/utils/logger"; import { - authorEmail, - authorName, - authorWebsite, - contributors, - dependencies, - description, - devDependencies, - license, - version, + authorEmail, + authorName, + authorWebsite, + contributors, + dependencies, + description, + devDependencies, + license, + version, } from "~/core/utils/package-json"; import { responseHandler } from "~/core/utils/response-handler"; - import { backupDir } from "~/core/database/backup"; import { hashApiKey } from "~/middleware/auth"; import type { config } from "~/typings/database"; export const apiConfigRoutes = new Elysia({ prefix: "/config" }) - .get( - "", - async ({ set }) => { - try { - const data = dbFunctions.getConfig() as config[]; - const distinct = data[0]; - set.status = 200; + .get( + "", + async ({ set }) => { + try { + const data = dbFunctions.getConfig() as config[]; + const distinct = data[0]; + set.status = 200; - logger.debug("Fetched backend config"); - return distinct; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Returns current API configuration including data retention policies and security settings", - responses: { - "200": { - description: "Successfully retrieved configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - fetching_interval: { - type: "number", - example: 5, - }, - keep_data_for: { - type: "number", - example: 7, - }, - api_key: { - type: "string", - example: "hashed_api_key", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error getting the DockStatAPI config", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - .get( - "/plugins", - ({ set }) => { - try { - return pluginManager.getLoadedPlugins(); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Lists all active plugins with their registration details and status", - responses: { - "200": { - description: "Successfully retrieved plugins", - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "object", - properties: { - name: { - type: "string", - example: "example-plugin", - }, - version: { - type: "string", - example: "1.0.0", - }, - status: { - type: "string", - example: "active", - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving plugins", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error getting all registered plugins", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - .post( - "/update", - async ({ set, body }) => { - try { - const { fetching_interval, keep_data_for, api_key } = body; + logger.debug("Fetched backend config"); + return distinct; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Returns current API configuration including data retention policies and security settings", + responses: { + "200": { + description: "Successfully retrieved configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + fetching_interval: { + type: "number", + example: 5, + }, + keep_data_for: { + type: "number", + example: 7, + }, + api_key: { + type: "string", + example: "hashed_api_key", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error getting the DockStatAPI config", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) + .get( + "/plugins", + ({ set }) => { + try { + return pluginManager.getLoadedPlugins(); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Lists all active plugins with their registration details and status", + responses: { + "200": { + description: "Successfully retrieved plugins", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "object", + properties: { + name: { + type: "string", + example: "example-plugin", + }, + version: { + type: "string", + example: "1.0.0", + }, + status: { + type: "string", + example: "active", + }, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving plugins", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error getting all registered plugins", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) + .post( + "/update", + async ({ set, body }) => { + try { + const { fetching_interval, keep_data_for, api_key } = body; - dbFunctions.updateConfig( - fetching_interval, - keep_data_for, - await hashApiKey(api_key), - ); - return responseHandler.ok(set, "Updated DockStatAPI config"); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Modifies core API settings including data collection intervals, retention periods, and security credentials", - responses: { - "200": { - description: "Successfully updated configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Updated DockStatAPI config", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error updating configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error updating the DockStatAPI config", - }, - }, - }, - }, - }, - }, - }, - }, - body: t.Object({ - fetching_interval: t.Number(), - keep_data_for: t.Number(), - api_key: t.String(), - }), - }, - ) - .get( - "/package", - async () => { - try { - logger.debug("Fetching package.json"); - const data = { - version: version, - description: description, - license: license, - authorName: authorName, - authorEmail: authorEmail, - authorWebsite: authorWebsite, - contributors: contributors, - dependencies: dependencies, - devDependencies: devDependencies, - }; + dbFunctions.updateConfig( + fetching_interval, + keep_data_for, + await hashApiKey(api_key) + ); + return responseHandler.ok(set, "Updated DockStatAPI config"); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Modifies core API settings including data collection intervals, retention periods, and security credentials", + responses: { + "200": { + description: "Successfully updated configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Updated DockStatAPI config", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error updating configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error updating the DockStatAPI config", + }, + }, + }, + }, + }, + }, + }, + }, + body: t.Object({ + fetching_interval: t.Number(), + keep_data_for: t.Number(), + api_key: t.String(), + }), + } + ) + .get( + "/package", + async () => { + try { + logger.debug("Fetching package.json"); + const data = { + version: version, + description: description, + license: license, + authorName: authorName, + authorEmail: authorEmail, + authorWebsite: authorWebsite, + contributors: contributors, + dependencies: dependencies, + devDependencies: devDependencies, + }; - logger.debug( - `Received: ${JSON.stringify(data).length} chars in package.json`, - ); + logger.debug( + `Received: ${JSON.stringify(data).length} chars in package.json` + ); - if (JSON.stringify(data).length <= 10) { - throw new Error("Failed to read package.json"); - } + if (JSON.stringify(data).length <= 10) { + throw new Error("Failed to read package.json"); + } - return data; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Displays package metadata including dependencies, contributors, and licensing information", - responses: { - "200": { - description: "Successfully retrieved package information", - content: { - "application/json": { - schema: { - type: "object", - properties: { - version: { - type: "string", - example: "3.0.0", - }, - description: { - type: "string", - example: - "DockStatAPI is an API backend featuring plugins and more for DockStat", - }, - license: { - type: "string", - example: "CC BY-NC 4.0", - }, - authorName: { - type: "string", - example: "ItsNik", - }, - authorEmail: { - type: "string", - example: "info@itsnik.de", - }, - authorWebsite: { - type: "string", - example: "https://github.com/Its4Nik", - }, - contributors: { - type: "array", - items: { - type: "string", - }, - example: [], - }, - dependencies: { - type: "object", - example: { - "@elysiajs/server-timing": "^1.2.1", - "@elysiajs/static": "^1.2.0", - }, - }, - devDependencies: { - type: "object", - example: { - "@biomejs/biome": "1.9.4", - "@types/dockerode": "^3.3.38", - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving package information", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error while reading package.json", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - .post( - "/backup", - async ({ set }) => { - try { - const backupFilename = await dbFunctions.backupDatabase(); - return responseHandler.ok(set, backupFilename); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: "Backs up the internal database", - responses: { - "200": { - description: "Successfully created backup", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "backup_2024-03-20_12-00-00.db.bak", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error creating backup", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error backing up", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - .get( - "/backup", - async ({ set }) => { - try { - const backupFiles = readdirSync(backupDir); + return data; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Displays package metadata including dependencies, contributors, and licensing information", + responses: { + "200": { + description: "Successfully retrieved package information", + content: { + "application/json": { + schema: { + type: "object", + properties: { + version: { + type: "string", + example: "3.0.0", + }, + description: { + type: "string", + example: + "DockStatAPI is an API backend featuring plugins and more for DockStat", + }, + license: { + type: "string", + example: "CC BY-NC 4.0", + }, + authorName: { + type: "string", + example: "ItsNik", + }, + authorEmail: { + type: "string", + example: "info@itsnik.de", + }, + authorWebsite: { + type: "string", + example: "https://github.com/Its4Nik", + }, + contributors: { + type: "array", + items: { + type: "string", + }, + example: [], + }, + dependencies: { + type: "object", + example: { + "@elysiajs/server-timing": "^1.2.1", + "@elysiajs/static": "^1.2.0", + }, + }, + devDependencies: { + type: "object", + example: { + "@biomejs/biome": "1.9.4", + "@types/dockerode": "^3.3.38", + }, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving package information", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error while reading package.json", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) + .post( + "/backup", + async ({ set }) => { + try { + const backupFilename = await dbFunctions.backupDatabase(); + return responseHandler.ok(set, backupFilename); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: "Backs up the internal database", + responses: { + "200": { + description: "Successfully created backup", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "backup_2024-03-20_12-00-00.db.bak", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error creating backup", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error backing up", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) + .get( + "/backup", + async ({ set }) => { + try { + const backupFiles = readdirSync(backupDir); - const filteredFiles = backupFiles.filter((file: string) => { - return !( - file.endsWith(".db") || - file.endsWith(".db-shm") || - file.endsWith(".db-wal") - ); - }); + const filteredFiles = backupFiles.filter((file: string) => { + return !( + file.endsWith(".db") || + file.endsWith(".db-shm") || + file.endsWith(".db-wal") + ); + }); - return filteredFiles; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: "Lists all available backups", - responses: { - "200": { - description: "Successfully retrieved backup list", - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "string", - }, - example: [ - "backup_2024-03-20_12-00-00.db.bak", - "backup_2024-03-19_12-00-00.db.bak", - ], - }, - }, - }, - }, - "400": { - description: "Error retrieving backup list", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Reading Backup directory", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) + return filteredFiles; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: "Lists all available backups", + responses: { + "200": { + description: "Successfully retrieved backup list", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "string", + }, + example: [ + "backup_2024-03-20_12-00-00.db.bak", + "backup_2024-03-19_12-00-00.db.bak", + ], + }, + }, + }, + }, + "400": { + description: "Error retrieving backup list", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Reading Backup directory", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) - .get( - "/backup/download", - async ({ query, set }) => { - try { - const filename = query.filename || dbFunctions.findLatestBackup(); - const filePath = `${backupDir}/${filename}`; + .get( + "/backup/download", + async ({ query, set }) => { + try { + const filename = query.filename || dbFunctions.findLatestBackup(); + const filePath = `${backupDir}/${filename}`; - if (!existsSync(filePath)) { - throw new Error("Backup file not found"); - } + if (!existsSync(filePath)) { + throw new Error("Backup file not found"); + } - set.headers["Content-Type"] = "application/octet-stream"; - set.headers["Content-Disposition"] = - `attachment; filename="${filename}"`; - return Bun.file(filePath); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Download a specific backup or the latest if no filename is provided", - responses: { - "200": { - description: "Successfully downloaded backup file", - content: { - "application/octet-stream": { - schema: { - type: "string", - format: "binary", - example: "Binary backup file content", - }, - }, - }, - headers: { - "Content-Disposition": { - schema: { - type: "string", - example: - 'attachment; filename="backup_2024-03-20_12-00-00.db.bak"', - }, - }, - }, - }, - "400": { - description: "Error downloading backup", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Backup download failed", - }, - }, - }, - }, - }, - }, - }, - }, - query: t.Object({ - filename: t.Optional(t.String()), - }), - }, - ) - .post( - "/restore", - async ({ body, set }) => { - try { - const { file } = body; + set.headers["Content-Type"] = "application/octet-stream"; + set.headers[ + "Content-Disposition" + ] = `attachment; filename="${filename}"`; + return Bun.file(filePath); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Download a specific backup or the latest if no filename is provided", + responses: { + "200": { + description: "Successfully downloaded backup file", + content: { + "application/octet-stream": { + schema: { + type: "string", + format: "binary", + example: "Binary backup file content", + }, + }, + }, + headers: { + "Content-Disposition": { + schema: { + type: "string", + example: + 'attachment; filename="backup_2024-03-20_12-00-00.db.bak"', + }, + }, + }, + }, + "400": { + description: "Error downloading backup", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Backup download failed", + }, + }, + }, + }, + }, + }, + }, + }, + query: t.Object({ + filename: t.Optional(t.String()), + }), + } + ) + .post( + "/restore", + async ({ body, set }) => { + try { + const { file } = body; - set.headers["Content-Type"] = "text/html"; + set.headers["Content-Type"] = "text/html"; - if (!file) { - throw new Error("No file uploaded"); - } + if (!file) { + throw new Error("No file uploaded"); + } - if (!file.name.endsWith(".db.bak")) { - throw new Error("Invalid file type. Expected .db.bak"); - } + if (!file.name.endsWith(".db.bak")) { + throw new Error("Invalid file type. Expected .db.bak"); + } - const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; - const fileBuffer = await file.arrayBuffer(); + const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; + const fileBuffer = await file.arrayBuffer(); - await Bun.write(tempPath, fileBuffer); - dbFunctions.restoreDatabase(tempPath); - unlinkSync(tempPath); + await Bun.write(tempPath, fileBuffer); + dbFunctions.restoreDatabase(tempPath); + unlinkSync(tempPath); - return responseHandler.ok(set, "Database restored successfully"); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - body: t.Object({ file: t.File() }), - detail: { - tags: ["Management"], - description: "Restore database from uploaded backup file", - responses: { - "200": { - description: "Successfully restored database", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Database restored successfully", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error restoring database", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Database restoration error", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ); + return responseHandler.ok(set, "Database restored successfully"); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + body: t.Object({ file: t.File() }), + detail: { + tags: ["Management"], + description: "Restore database from uploaded backup file", + responses: { + "200": { + description: "Successfully restored database", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Database restored successfully", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error restoring database", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Database restoration error", + }, + }, + }, + }, + }, + }, + }, + }, + } + ); diff --git a/src/routes/docker-manager.ts b/src/routes/docker-manager.ts index 30cd5c4..279fa2b 100644 --- a/src/routes/docker-manager.ts +++ b/src/routes/docker-manager.ts @@ -1,269 +1,255 @@ import { Elysia, t } from "elysia"; - +import type { DockerHost } from "~/typings/docker"; import { dbFunctions } from "~/core/database"; import { logger } from "~/core/utils/logger"; import { responseHandler } from "~/core/utils/response-handler"; -import type { DockerHost } from "~/typings/docker"; - export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) - .post( - "/add-host", - async ({ set, body }) => { - try { - dbFunctions.addDockerHost(body as DockerHost); - return responseHandler.ok(set, `Added docker host (${body.name})`); - } catch (error: unknown) { - return responseHandler.error( - set, - "Error adding docker Host", - error as string, - ); - } - }, - { - detail: { - tags: ["Management"], - description: - "Registers a new Docker host to the monitoring system with connection details", - responses: { - "200": { - description: "Successfully added Docker host", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Added docker host (Localhost)", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error adding Docker host", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error adding docker Host", - }, - }, - }, - }, - }, - }, - }, - }, - body: t.Object({ - name: t.String(), - hostAddress: t.String(), - secure: t.Boolean(), - }), - }, - ) + .post( + "/add-host", + async ({ set, body }) => { + try { + dbFunctions.addDockerHost(body as DockerHost); + return responseHandler.ok(set, `Added docker host (${body.name})`); + } catch (error: unknown) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Registers a new Docker host to the monitoring system with connection details", + responses: { + "200": { + description: "Successfully added Docker host", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Added docker host (Localhost)", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error adding Docker host", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error adding docker Host", + }, + }, + }, + }, + }, + }, + }, + }, + body: t.Object({ + name: t.String(), + hostAddress: t.String(), + secure: t.Boolean(), + }), + } + ) - .post( - "/update-host", - async ({ set, body }) => { - try { - set.status = 200; - dbFunctions.updateDockerHost(body); - return responseHandler.ok(set, `Updated docker host (${body.id})`); - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to update host", - ); - } - }, - { - detail: { - tags: ["Management"], - description: - "Modifies existing Docker host configuration parameters (name, address, security)", - responses: { - "200": { - description: "Successfully updated Docker host", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Updated docker host (1)", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error updating Docker host", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to update host", - }, - }, - }, - }, - }, - }, - }, - }, - body: t.Object({ - id: t.Number(), - name: t.String(), - hostAddress: t.String(), - secure: t.Boolean(), - }), - }, - ) + .post( + "/update-host", + async ({ set, body }) => { + try { + set.status = 200; + dbFunctions.updateDockerHost(body); + return responseHandler.ok(set, `Updated docker host (${body.id})`); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Modifies existing Docker host configuration parameters (name, address, security)", + responses: { + "200": { + description: "Successfully updated Docker host", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Updated docker host (1)", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error updating Docker host", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to update host", + }, + }, + }, + }, + }, + }, + }, + }, + body: t.Object({ + id: t.Number(), + name: t.String(), + hostAddress: t.String(), + secure: t.Boolean(), + }), + } + ) - .get( - "/hosts", - async ({ set }) => { - try { - const dockerHosts = dbFunctions.getDockerHosts(); + .get( + "/hosts", + async ({ set }) => { + try { + const dockerHosts = dbFunctions.getDockerHosts(); - logger.debug("Retrieved docker hosts"); - return dockerHosts; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to retrieve hosts", - ); - } - }, - { - detail: { - tags: ["Management"], - description: - "Lists all configured Docker hosts with their connection settings", - responses: { - "200": { - description: "Successfully retrieved Docker hosts", - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "object", - properties: { - id: { - type: "number", - example: 1, - }, - name: { - type: "string", - example: "Localhost", - }, - hostAddress: { - type: "string", - example: "localhost:2375", - }, - secure: { - type: "boolean", - example: false, - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving Docker hosts", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve hosts", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) + logger.debug("Retrieved docker hosts"); + return dockerHosts; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Lists all configured Docker hosts with their connection settings", + responses: { + "200": { + description: "Successfully retrieved Docker hosts", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "object", + properties: { + id: { + type: "number", + example: 1, + }, + name: { + type: "string", + example: "Localhost", + }, + hostAddress: { + type: "string", + example: "localhost:2375", + }, + secure: { + type: "boolean", + example: false, + }, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving Docker hosts", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve hosts", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) - .delete( - "/hosts/:id", - async ({ set, params }) => { - try { - set.status = 200; - dbFunctions.deleteDockerHost(params.id); - return responseHandler.ok(set, `Deleted docker host (${params.id})`); - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to delete host", - ); - } - }, - { - detail: { - tags: ["Management"], - description: - "Removes Docker host from monitoring system and clears associated data", - responses: { - "200": { - description: "Successfully deleted Docker host", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Deleted docker host (1)", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error deleting Docker host", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to delete host", - }, - }, - }, - }, - }, - }, - }, - }, - params: t.Object({ - id: t.Number(), - }), - }, - ); + .delete( + "/hosts/:id", + async ({ set, params }) => { + try { + set.status = 200; + dbFunctions.deleteDockerHost(params.id); + return responseHandler.ok(set, `Deleted docker host (${params.id})`); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Removes Docker host from monitoring system and clears associated data", + responses: { + "200": { + description: "Successfully deleted Docker host", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Deleted docker host (1)", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error deleting Docker host", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to delete host", + }, + }, + }, + }, + }, + }, + }, + }, + params: t.Object({ + id: t.Number(), + }), + } + ); diff --git a/src/routes/docker-stats.ts b/src/routes/docker-stats.ts index b411f2d..f9d966d 100644 --- a/src/routes/docker-stats.ts +++ b/src/routes/docker-stats.ts @@ -1,601 +1,598 @@ import type Docker from "dockerode"; import { Elysia } from "elysia"; - import { dbFunctions } from "~/core/database"; import { getDockerClient } from "~/core/docker/client"; import { - calculateCpuPercent, - calculateMemoryUsage, + calculateCpuPercent, + calculateMemoryUsage, } from "~/core/utils/calculations"; import { findObjectByKey } from "~/core/utils/helpers"; import { logger } from "~/core/utils/logger"; import { responseHandler } from "~/core/utils/response-handler"; - import type { ContainerInfo, DockerHost, HostStats } from "~/typings/docker"; import type { DockerInfo } from "~/typings/dockerode"; export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) - .get( - "/containers", - async ({ set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const containers: ContainerInfo[] = []; + .get( + "/containers", + async ({ set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + const containers: ContainerInfo[] = []; - await Promise.all( - hosts.map(async (host) => { - try { - const docker = getDockerClient(host); - try { - await docker.ping(); - } catch (pingError) { - return responseHandler.error( - set, - pingError as string, - "Docker host connection failed", - ); - } + await Promise.all( + hosts.map(async (host) => { + try { + const docker = getDockerClient(host); + try { + await docker.ping(); + } catch (pingError) { + return responseHandler.error( + set, + pingError as string, + "Docker host connection failed" + ); + } - const hostContainers = await docker.listContainers({ all: true }); + const hostContainers = await docker.listContainers({ all: true }); - await Promise.all( - hostContainers.map(async (containerInfo) => { - try { - const container = docker.getContainer(containerInfo.Id); - const stats = await new Promise( - (resolve, reject) => { - container.stats({ stream: false }, (error, stats) => { - if (error) { - return responseHandler.reject( - set, - reject, - "An error occurred", - error, - ); - } - if (!stats) { - return responseHandler.reject( - set, - reject, - "No stats available", - ); - } - resolve(stats); - }); - }, - ); + await Promise.all( + hostContainers.map(async (containerInfo) => { + try { + const container = docker.getContainer(containerInfo.Id); + const stats = await new Promise( + (resolve, reject) => { + container.stats({ stream: false }, (error, stats) => { + if (error) { + return responseHandler.reject( + set, + reject, + "An error occurred", + error + ); + } + if (!stats) { + return responseHandler.reject( + set, + reject, + "No stats available" + ); + } + resolve(stats); + }); + } + ); - containers.push({ - id: containerInfo.Id, - hostId: `${host.id}`, - name: containerInfo.Names[0].replace(/^\//, ""), - image: containerInfo.Image, - status: containerInfo.Status, - state: containerInfo.State, - cpuUsage: calculateCpuPercent(stats), - memoryUsage: calculateMemoryUsage(stats), - stats: stats, - info: containerInfo, - }); - } catch (containerError) { - logger.error( - "Error fetching container stats,", - containerError, - ); - } - }), - ); - logger.debug(`Fetched stats for ${host.name}`); - } catch (hostError) { - logger.error("Error fetching containers for host,", hostError); - } - }), - ); + containers.push({ + id: containerInfo.Id, + hostId: `${host.id}`, + name: containerInfo.Names[0].replace(/^\//, ""), + image: containerInfo.Image, + status: containerInfo.Status, + state: containerInfo.State, + cpuUsage: calculateCpuPercent(stats), + memoryUsage: calculateMemoryUsage(stats), + stats: stats, + info: containerInfo, + }); + } catch (containerError) { + logger.error( + "Error fetching container stats,", + containerError + ); + } + }) + ); + logger.debug(`Fetched stats for ${host.name}`); + } catch (error) { + const errMsg = + error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }) + ); - logger.debug("Fetched all containers across all hosts"); - return { containers }; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to retrieve containers", - ); - } - }, - { - detail: { - tags: ["Statistics"], - description: - "Collects real-time statistics for all Docker containers across monitored hosts, including CPU and memory utilization", - responses: { - "200": { - description: "Successfully retrieved container statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - containers: { - type: "array", - items: { - type: "object", - properties: { - id: { - type: "string", - example: "abc123def456", - }, - hostId: { - type: "string", - example: "1", - }, - name: { - type: "string", - example: "example-container", - }, - image: { - type: "string", - example: "nginx:latest", - }, - status: { - type: "string", - example: "running", - }, - state: { - type: "string", - example: "running", - }, - cpuUsage: { - type: "number", - example: 0.5, - }, - memoryUsage: { - type: "number", - example: 1024, - }, - }, - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving container statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve containers", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - .get( - "/hosts", - async ({ set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + logger.debug("Fetched all containers across all hosts"); + return { containers }; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Statistics"], + description: + "Collects real-time statistics for all Docker containers across monitored hosts, including CPU and memory utilization", + responses: { + "200": { + description: "Successfully retrieved container statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + containers: { + type: "array", + items: { + type: "object", + properties: { + id: { + type: "string", + example: "abc123def456", + }, + hostId: { + type: "string", + example: "1", + }, + name: { + type: "string", + example: "example-container", + }, + image: { + type: "string", + example: "nginx:latest", + }, + status: { + type: "string", + example: "running", + }, + state: { + type: "string", + example: "running", + }, + cpuUsage: { + type: "number", + example: 0.5, + }, + memoryUsage: { + type: "number", + example: 1024, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving container statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve containers", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) + .get( + "/hosts", + async ({ set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const stats: HostStats[] = []; + const stats: HostStats[] = []; - for (const host of hosts) { - const docker = getDockerClient(host); - const info: DockerInfo = await docker.info(); + for (const host of hosts) { + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); - const config: HostStats = { - hostId: host.id as number, - hostName: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - labels: info.Labels, - images: info.Images, - containers: info.Containers, - containersPaused: info.ContainersPaused, - containersRunning: info.ContainersRunning, - containersStopped: info.ContainersStopped, - }; + const config: HostStats = { + hostId: host.id as number, + hostName: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; - stats.push(config); - } + stats.push(config); + } - logger.debug("Fetched all hosts"); - return stats; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to retrieve host config", - ); - } - }, - { - detail: { - tags: ["Statistics"], - description: - "Provides detailed system metrics and Docker runtime information for specified host", - responses: { - "200": { - description: "Successfully retrieved host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - hostId: { - type: "number", - example: 1, - }, - hostName: { - type: "string", - example: "Localhost", - }, - dockerVersion: { - type: "string", - example: "24.0.5", - }, - apiVersion: { - type: "string", - example: "1.41", - }, - os: { - type: "string", - example: "Linux", - }, - architecture: { - type: "string", - example: "x86_64", - }, - totalMemory: { - type: "number", - example: 16777216, - }, - totalCPU: { - type: "number", - example: 4, - }, - labels: { - type: "array", - items: { - type: "string", - }, - example: ["environment=production"], - }, - images: { - type: "number", - example: 10, - }, - containers: { - type: "number", - example: 5, - }, - containersPaused: { - type: "number", - example: 0, - }, - containersRunning: { - type: "number", - example: 4, - }, - containersStopped: { - type: "number", - example: 1, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve host config", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - .get( - "/hosts", - async ({ set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + logger.debug("Fetched all hosts"); + return stats; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to retrieve host config" + ); + } + }, + { + detail: { + tags: ["Statistics"], + description: + "Provides detailed system metrics and Docker runtime information for specified host", + responses: { + "200": { + description: "Successfully retrieved host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + hostId: { + type: "number", + example: 1, + }, + hostName: { + type: "string", + example: "Localhost", + }, + dockerVersion: { + type: "string", + example: "24.0.5", + }, + apiVersion: { + type: "string", + example: "1.41", + }, + os: { + type: "string", + example: "Linux", + }, + architecture: { + type: "string", + example: "x86_64", + }, + totalMemory: { + type: "number", + example: 16777216, + }, + totalCPU: { + type: "number", + example: 4, + }, + labels: { + type: "array", + items: { + type: "string", + }, + example: ["environment=production"], + }, + images: { + type: "number", + example: 10, + }, + containers: { + type: "number", + example: 5, + }, + containersPaused: { + type: "number", + example: 0, + }, + containersRunning: { + type: "number", + example: 4, + }, + containersStopped: { + type: "number", + example: 1, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve host config", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) + .get( + "/hosts", + async ({ set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const stats: HostStats[] = []; + const stats: HostStats[] = []; - for (const host of hosts) { - const docker = getDockerClient(host); - const info: DockerInfo = await docker.info(); + for (const host of hosts) { + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); - const config: HostStats = { - hostId: host.id as number, - hostName: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - labels: info.Labels, - images: info.Images, - containers: info.Containers, - containersPaused: info.ContainersPaused, - containersRunning: info.ContainersRunning, - containersStopped: info.ContainersStopped, - }; + const config: HostStats = { + hostId: host.id as number, + hostName: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; - stats.push(config); - } + stats.push(config); + } - logger.debug("Fetched stats for all hosts"); - return stats; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to retrieve host config", - ); - } - }, - { - detail: { - tags: ["Statistics"], - description: - "Provides detailed system metrics and Docker runtime information for all hosts", - responses: { - "200": { - description: "Successfully retrieved host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - hostId: { - type: "number", - example: 1, - }, - hostName: { - type: "string", - example: "Localhost", - }, - dockerVersion: { - type: "string", - example: "24.0.5", - }, - apiVersion: { - type: "string", - example: "1.41", - }, - os: { - type: "string", - example: "Linux", - }, - architecture: { - type: "string", - example: "x86_64", - }, - totalMemory: { - type: "number", - example: 16777216, - }, - totalCPU: { - type: "number", - example: 4, - }, - labels: { - type: "array", - items: { - type: "string", - }, - example: ["environment=production"], - }, - images: { - type: "number", - example: 10, - }, - containers: { - type: "number", - example: 5, - }, - containersPaused: { - type: "number", - example: 0, - }, - containersRunning: { - type: "number", - example: 4, - }, - containersStopped: { - type: "number", - example: 1, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve host config", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - .get( - "/hosts/:id", - async ({ params, set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + logger.debug("Fetched stats for all hosts"); + return stats; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to retrieve host config" + ); + } + }, + { + detail: { + tags: ["Statistics"], + description: + "Provides detailed system metrics and Docker runtime information for all hosts", + responses: { + "200": { + description: "Successfully retrieved host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + hostId: { + type: "number", + example: 1, + }, + hostName: { + type: "string", + example: "Localhost", + }, + dockerVersion: { + type: "string", + example: "24.0.5", + }, + apiVersion: { + type: "string", + example: "1.41", + }, + os: { + type: "string", + example: "Linux", + }, + architecture: { + type: "string", + example: "x86_64", + }, + totalMemory: { + type: "number", + example: 16777216, + }, + totalCPU: { + type: "number", + example: 4, + }, + labels: { + type: "array", + items: { + type: "string", + }, + example: ["environment=production"], + }, + images: { + type: "number", + example: 10, + }, + containers: { + type: "number", + example: 5, + }, + containersPaused: { + type: "number", + example: 0, + }, + containersRunning: { + type: "number", + example: 4, + }, + containersStopped: { + type: "number", + example: 1, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve host config", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) + .get( + "/hosts/:id", + async ({ params, set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const host = findObjectByKey(hosts, "id", Number(params.id)); - if (!host) { - return responseHandler.simple_error( - set, - `Host (${params.id}) not found`, - ); - } + const host = findObjectByKey(hosts, "id", Number(params.id)); + if (!host) { + return responseHandler.simple_error( + set, + `Host (${params.id}) not found` + ); + } - const docker = getDockerClient(host); - const info: DockerInfo = await docker.info(); + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); - const config: HostStats = { - hostId: host.id as number, - hostName: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - labels: info.Labels, - images: info.Images, - containers: info.Containers, - containersPaused: info.ContainersPaused, - containersRunning: info.ContainersRunning, - containersStopped: info.ContainersStopped, - }; + const config: HostStats = { + hostId: host.id as number, + hostName: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; - logger.debug(`Fetched config for ${host.name}`); - return config; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to retrieve host config", - ); - } - }, - { - detail: { - tags: ["Statistics"], - description: - "Provides detailed system metrics and Docker runtime information for specified host", - responses: { - "200": { - description: "Successfully retrieved host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - hostId: { - type: "number", - example: 1, - }, - hostName: { - type: "string", - example: "Localhost", - }, - dockerVersion: { - type: "string", - example: "24.0.5", - }, - apiVersion: { - type: "string", - example: "1.41", - }, - os: { - type: "string", - example: "Linux", - }, - architecture: { - type: "string", - example: "x86_64", - }, - totalMemory: { - type: "number", - example: 16777216, - }, - totalCPU: { - type: "number", - example: 4, - }, - labels: { - type: "array", - items: { - type: "string", - }, - example: ["environment=production"], - }, - images: { - type: "number", - example: 10, - }, - containers: { - type: "number", - example: 5, - }, - containersPaused: { - type: "number", - example: 0, - }, - containersRunning: { - type: "number", - example: 4, - }, - containersStopped: { - type: "number", - example: 1, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve host config", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ); + logger.debug(`Fetched config for ${host.name}`); + return config; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to retrieve host config" + ); + } + }, + { + detail: { + tags: ["Statistics"], + description: + "Provides detailed system metrics and Docker runtime information for specified host", + responses: { + "200": { + description: "Successfully retrieved host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + hostId: { + type: "number", + example: 1, + }, + hostName: { + type: "string", + example: "Localhost", + }, + dockerVersion: { + type: "string", + example: "24.0.5", + }, + apiVersion: { + type: "string", + example: "1.41", + }, + os: { + type: "string", + example: "Linux", + }, + architecture: { + type: "string", + example: "x86_64", + }, + totalMemory: { + type: "number", + example: 16777216, + }, + totalCPU: { + type: "number", + example: 4, + }, + labels: { + type: "array", + items: { + type: "string", + }, + example: ["environment=production"], + }, + images: { + type: "number", + example: 10, + }, + containers: { + type: "number", + example: 5, + }, + containersPaused: { + type: "number", + example: 0, + }, + containersRunning: { + type: "number", + example: 4, + }, + containersStopped: { + type: "number", + example: 1, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve host config", + }, + }, + }, + }, + }, + }, + }, + }, + } + ); diff --git a/src/tests/docker-manager.spec.ts b/src/tests/docker-manager.spec.ts index df9d65d..64e04d2 100644 --- a/src/tests/docker-manager.spec.ts +++ b/src/tests/docker-manager.spec.ts @@ -3,462 +3,462 @@ import { Elysia } from "elysia"; import { dbFunctions } from "~/core/database"; import { dockerRoutes } from "~/routes/docker-manager"; import { - generateMarkdownReport, - recordTestResult, - testResults, + generateMarkdownReport, + recordTestResult, + testResults, } from "./markdown-exporter"; import type { TestContext } from "./markdown-exporter"; type DockerHost = { - id?: number; - name: string; - hostAddress: string; - secure: boolean; + id?: number; + name: string; + hostAddress: string; + secure: boolean; }; const mockDb = { - addDockerHost: mock(() => ({ - changes: 1, - lastInsertRowid: 1, - })), - updateDockerHost: mock(() => ({ - changes: 1, - lastInsertRowid: 1, - })), - getDockerHosts: mock(() => []), - deleteDockerHost: mock(() => ({ - changes: 1, - lastInsertRowid: 1, - })), + addDockerHost: mock(() => ({ + changes: 1, + lastInsertRowid: 1, + })), + updateDockerHost: mock(() => ({ + changes: 1, + lastInsertRowid: 1, + })), + getDockerHosts: mock(() => []), + deleteDockerHost: mock(() => ({ + changes: 1, + lastInsertRowid: 1, + })), }; mock.module("~/core/database", () => ({ - dbFunctions: mockDb, + dbFunctions: mockDb, })); mock.module("~/core/utils/logger", () => ({ - logger: { - debug: mock(), - info: mock(), - error: mock(), - }, + logger: { + debug: mock(), + info: mock(), + error: mock(), + }, })); const createApp = () => new Elysia().use(dockerRoutes).decorate({}); async function captureTestContext( - req: Request, - res: Response, + req: Request, + res: Response ): Promise { - const responseStatus = res.status; - const responseHeaders = Object.fromEntries(res.headers.entries()); - let responseBody: unknown; - - try { - responseBody = await res.clone().json(); - } catch (parseError) { - try { - responseBody = await res.clone().text(); - } catch { - responseBody = "Unparseable response content"; - } - } - - return { - request: { - method: req.method, - url: req.url, - headers: Object.fromEntries(req.headers.entries()), - body: req.body ? await req.clone().text() : undefined, - }, - response: { - status: responseStatus, - headers: responseHeaders, - body: responseBody, - }, - }; + const responseStatus = res.status; + const responseHeaders = Object.fromEntries(res.headers.entries()); + let responseBody: unknown; + + try { + responseBody = await res.clone().json(); + } catch (parseError) { + try { + responseBody = await res.clone().text(); + } catch { + responseBody = "Unparseable response content"; + } + } + + return { + request: { + method: req.method, + url: req.url, + headers: Object.fromEntries(req.headers.entries()), + body: req.body ? await req.clone().text() : undefined, + }, + response: { + status: responseStatus, + headers: responseHeaders, + body: responseBody, + }, + }; } describe("Docker Configuration Endpoints", () => { - beforeEach(() => { - mockDb.addDockerHost.mockClear(); - mockDb.updateDockerHost.mockClear(); - mockDb.getDockerHosts.mockClear(); - mockDb.deleteDockerHost.mockClear(); - }); - - describe("POST /docker-config/add-host", () => { - it("should add a docker host successfully", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const host: DockerHost = { - name: "Host1", - hostAddress: "127.0.0.1:2375", - secure: false, - }; - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/add-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - message: `Added docker host (${host.name})`, - }); - expect(mockDb.addDockerHost).toHaveBeenCalledWith(host); - - recordTestResult({ - name: "add-host success", - suite: "Docker Config - Add Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "add-host success", - suite: "Docker Config - Add Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with success message", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle error when adding a docker host fails", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const host: DockerHost = { - name: "Host2", - hostAddress: "invalid", - secure: true, - }; - - // Set mock implementation - mockDb.addDockerHost.mockImplementationOnce(() => { - throw new Error("DB error"); - }); - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/add-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - error: expect.any(String), - }); - - recordTestResult({ - name: "add-host failure", - suite: "Docker Config - Add Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "add-host failure", - suite: "Docker Config - Add Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "400 Error with error structure", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("POST /docker-config/update-host", () => { - it("should update a docker host successfully", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const host: DockerHost = { - id: 1, - name: "Host1-upd", - hostAddress: "127.0.0.1:2376", - secure: true, - }; - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/update-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - message: `Updated docker host (${host.id})`, - }); - expect(mockDb.updateDockerHost).toHaveBeenCalledWith(host); - - recordTestResult({ - name: "update-host success", - suite: "Docker Config - Update Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "update-host success", - suite: "Docker Config - Update Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with update confirmation", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle error when update fails", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const host: DockerHost = { - id: 2, - name: "Host2", - hostAddress: "x", - secure: false, - }; - - mockDb.updateDockerHost.mockImplementationOnce(() => { - throw new Error("Update error"); - }); - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/update-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - error: expect.any(String), - }); - - recordTestResult({ - name: "update-host failure", - suite: "Docker Config - Update Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "update-host failure", - suite: "Docker Config - Update Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "400 Error with error details", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("GET /docker-config/hosts", () => { - it("should retrieve list of hosts", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const hosts: DockerHost[] = [ - { id: 1, name: "H1", hostAddress: "a", secure: false }, - ]; - - mockDb.getDockerHosts.mockImplementation(() => hosts as never[]); - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/hosts"); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toEqual(hosts); - - recordTestResult({ - name: "get-hosts success", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "get-hosts success", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with hosts array", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle error when retrieval fails", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - mockDb.getDockerHosts.mockImplementationOnce(() => { - throw new Error("Fetch error"); - }); - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/hosts"); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - error: expect.any(String), - }); - - recordTestResult({ - name: "get-hosts failure", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "get-hosts failure", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "400 Error with error details", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("DELETE /docker-config/hosts/:id", () => { - it("should delete a host successfully", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const id = 5; - - try { - const app = createApp(); - const req = new Request(`http://localhost/docker-config/hosts/${id}`, { - method: "DELETE", - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - message: `Deleted docker host (${id})`, - }); - expect(mockDb.deleteDockerHost).toHaveBeenCalledWith(id); - - recordTestResult({ - name: "delete-host success", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "delete-host success", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with deletion confirmation", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle error when delete fails", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const id = 6; - - mockDb.deleteDockerHost.mockImplementationOnce(() => { - throw new Error("Delete error"); - }); - - try { - const app = createApp(); - const req = new Request(`http://localhost/docker-config/hosts/${id}`, { - method: "DELETE", - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - error: expect.any(String), - }); - - recordTestResult({ - name: "delete-host failure", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "delete-host failure", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "400 Error with error details", - received: context?.response, - }, - }); - throw error; - } - }); - }); + beforeEach(() => { + mockDb.addDockerHost.mockClear(); + mockDb.updateDockerHost.mockClear(); + mockDb.getDockerHosts.mockClear(); + mockDb.deleteDockerHost.mockClear(); + }); + + describe("POST /docker-config/add-host", () => { + it("should add a docker host successfully", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + name: "Host1", + hostAddress: "127.0.0.1:2375", + secure: false, + }; + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/add-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + message: `Added docker host (${host.name})`, + }); + expect(mockDb.addDockerHost).toHaveBeenCalledWith(host); + + recordTestResult({ + name: "add-host success", + suite: "Docker Config - Add Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "add-host success", + suite: "Docker Config - Add Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with success message", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when adding a docker host fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + name: "Host2", + hostAddress: "invalid", + secure: true, + }; + + // Set mock implementation + mockDb.addDockerHost.mockImplementationOnce(() => { + throw new Error("DB error"); + }); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/add-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + message: expect.any(String), + }); + + recordTestResult({ + name: "add-host failure", + suite: "Docker Config - Add Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "add-host failure", + suite: "Docker Config - Add Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error structure", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("POST /docker-config/update-host", () => { + it("should update a docker host successfully", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + id: 1, + name: "Host1-upd", + hostAddress: "127.0.0.1:2376", + secure: true, + }; + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/update-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + message: `Updated docker host (${host.id})`, + }); + expect(mockDb.updateDockerHost).toHaveBeenCalledWith(host); + + recordTestResult({ + name: "update-host success", + suite: "Docker Config - Update Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "update-host success", + suite: "Docker Config - Update Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with update confirmation", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when update fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + id: 2, + name: "Host2", + hostAddress: "x", + secure: false, + }; + + mockDb.updateDockerHost.mockImplementationOnce(() => { + throw new Error("Update error"); + }); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/update-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + message: expect.any(String), + }); + + recordTestResult({ + name: "update-host failure", + suite: "Docker Config - Update Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "update-host failure", + suite: "Docker Config - Update Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error details", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("GET /docker-config/hosts", () => { + it("should retrieve list of hosts", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const hosts: DockerHost[] = [ + { id: 1, name: "H1", hostAddress: "a", secure: false }, + ]; + + mockDb.getDockerHosts.mockImplementation(() => hosts as never[]); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/hosts"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toEqual(hosts); + + recordTestResult({ + name: "get-hosts success", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "get-hosts success", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with hosts array", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when retrieval fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + mockDb.getDockerHosts.mockImplementationOnce(() => { + throw new Error("Fetch error"); + }); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/hosts"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + message: expect.any(String), + }); + + recordTestResult({ + name: "get-hosts failure", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "get-hosts failure", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error details", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("DELETE /docker-config/hosts/:id", () => { + it("should delete a host successfully", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const id = 5; + + try { + const app = createApp(); + const req = new Request(`http://localhost/docker-config/hosts/${id}`, { + method: "DELETE", + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + message: `Deleted docker host (${id})`, + }); + expect(mockDb.deleteDockerHost).toHaveBeenCalledWith(id); + + recordTestResult({ + name: "delete-host success", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "delete-host success", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with deletion confirmation", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when delete fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const id = 6; + + mockDb.deleteDockerHost.mockImplementationOnce(() => { + throw new Error("Delete error"); + }); + + try { + const app = createApp(); + const req = new Request(`http://localhost/docker-config/hosts/${id}`, { + method: "DELETE", + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + message: expect.any(String), + }); + + recordTestResult({ + name: "delete-host failure", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "delete-host failure", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error details", + received: context?.response, + }, + }); + throw error; + } + }); + }); }); afterAll(() => { - generateMarkdownReport(); + generateMarkdownReport(); }); From 7bb328b0f4a8770130e71eddc419bd926b7af90f Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Fri, 16 May 2025 08:33:33 +0000 Subject: [PATCH 323/324] CQL: Apply lint fixes [skip ci] --- src/core/utils/response-handler.ts | 68 +- src/routes/api-config.ts | 1115 ++++++++++++++------------- src/routes/docker-manager.ts | 490 ++++++------ src/routes/docker-stats.ts | 1132 ++++++++++++++-------------- src/tests/docker-manager.spec.ts | 866 ++++++++++----------- 5 files changed, 1835 insertions(+), 1836 deletions(-) diff --git a/src/core/utils/response-handler.ts b/src/core/utils/response-handler.ts index 990ae81..00d5b46 100644 --- a/src/core/utils/response-handler.ts +++ b/src/core/utils/response-handler.ts @@ -2,41 +2,41 @@ import { logger } from "~/core/utils/logger"; import type { set } from "~/typings/elysiajs"; export const responseHandler = { - error( - set: set, - error: string, - response_message: string, - error_code?: number - ) { - set.status = error_code || 500; - logger.error(`${response_message} - ${error}`); - return { success: false, message: response_message, error: String(error) }; - }, + error( + set: set, + error: string, + response_message: string, + error_code?: number, + ) { + set.status = error_code || 500; + logger.error(`${response_message} - ${error}`); + return { success: false, message: response_message, error: String(error) }; + }, - ok(set: set, response_message: string) { - set.status = 200; - logger.debug(response_message); - return { success: true, message: response_message }; - }, + ok(set: set, response_message: string) { + set.status = 200; + logger.debug(response_message); + return { success: true, message: response_message }; + }, - simple_error(set: set, response_message: string, status_code?: number) { - set.status = status_code || 502; - logger.warn(response_message); - return { success: false, message: response_message }; - }, + simple_error(set: set, response_message: string, status_code?: number) { + set.status = status_code || 502; + logger.warn(response_message); + return { success: false, message: response_message }; + }, - reject( - set: set, - reject: CallableFunction, - response_message: string, - error?: string - ) { - set.status = 501; - if (error) { - logger.error(`${response_message} - ${error}`); - } else { - logger.error(response_message); - } - return reject(new Error(response_message)); - }, + reject( + set: set, + reject: CallableFunction, + response_message: string, + error?: string, + ) { + set.status = 501; + if (error) { + logger.error(`${response_message} - ${error}`); + } else { + logger.error(response_message); + } + return reject(new Error(response_message)); + }, }; diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index bfbaa75..6e2de1e 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -1,586 +1,585 @@ import { existsSync, readdirSync, unlinkSync } from "node:fs"; import { Elysia, t } from "elysia"; import { dbFunctions } from "~/core/database"; +import { backupDir } from "~/core/database/backup"; import { pluginManager } from "~/core/plugins/plugin-manager"; import { logger } from "~/core/utils/logger"; import { - authorEmail, - authorName, - authorWebsite, - contributors, - dependencies, - description, - devDependencies, - license, - version, + authorEmail, + authorName, + authorWebsite, + contributors, + dependencies, + description, + devDependencies, + license, + version, } from "~/core/utils/package-json"; import { responseHandler } from "~/core/utils/response-handler"; -import { backupDir } from "~/core/database/backup"; import { hashApiKey } from "~/middleware/auth"; import type { config } from "~/typings/database"; export const apiConfigRoutes = new Elysia({ prefix: "/config" }) - .get( - "", - async ({ set }) => { - try { - const data = dbFunctions.getConfig() as config[]; - const distinct = data[0]; - set.status = 200; + .get( + "", + async ({ set }) => { + try { + const data = dbFunctions.getConfig() as config[]; + const distinct = data[0]; + set.status = 200; - logger.debug("Fetched backend config"); - return distinct; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Returns current API configuration including data retention policies and security settings", - responses: { - "200": { - description: "Successfully retrieved configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - fetching_interval: { - type: "number", - example: 5, - }, - keep_data_for: { - type: "number", - example: 7, - }, - api_key: { - type: "string", - example: "hashed_api_key", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error getting the DockStatAPI config", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - .get( - "/plugins", - ({ set }) => { - try { - return pluginManager.getLoadedPlugins(); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Lists all active plugins with their registration details and status", - responses: { - "200": { - description: "Successfully retrieved plugins", - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "object", - properties: { - name: { - type: "string", - example: "example-plugin", - }, - version: { - type: "string", - example: "1.0.0", - }, - status: { - type: "string", - example: "active", - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving plugins", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error getting all registered plugins", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - .post( - "/update", - async ({ set, body }) => { - try { - const { fetching_interval, keep_data_for, api_key } = body; + logger.debug("Fetched backend config"); + return distinct; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Returns current API configuration including data retention policies and security settings", + responses: { + "200": { + description: "Successfully retrieved configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + fetching_interval: { + type: "number", + example: 5, + }, + keep_data_for: { + type: "number", + example: 7, + }, + api_key: { + type: "string", + example: "hashed_api_key", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error getting the DockStatAPI config", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) + .get( + "/plugins", + ({ set }) => { + try { + return pluginManager.getLoadedPlugins(); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Lists all active plugins with their registration details and status", + responses: { + "200": { + description: "Successfully retrieved plugins", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "object", + properties: { + name: { + type: "string", + example: "example-plugin", + }, + version: { + type: "string", + example: "1.0.0", + }, + status: { + type: "string", + example: "active", + }, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving plugins", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error getting all registered plugins", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) + .post( + "/update", + async ({ set, body }) => { + try { + const { fetching_interval, keep_data_for, api_key } = body; - dbFunctions.updateConfig( - fetching_interval, - keep_data_for, - await hashApiKey(api_key) - ); - return responseHandler.ok(set, "Updated DockStatAPI config"); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Modifies core API settings including data collection intervals, retention periods, and security credentials", - responses: { - "200": { - description: "Successfully updated configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Updated DockStatAPI config", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error updating configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error updating the DockStatAPI config", - }, - }, - }, - }, - }, - }, - }, - }, - body: t.Object({ - fetching_interval: t.Number(), - keep_data_for: t.Number(), - api_key: t.String(), - }), - } - ) - .get( - "/package", - async () => { - try { - logger.debug("Fetching package.json"); - const data = { - version: version, - description: description, - license: license, - authorName: authorName, - authorEmail: authorEmail, - authorWebsite: authorWebsite, - contributors: contributors, - dependencies: dependencies, - devDependencies: devDependencies, - }; + dbFunctions.updateConfig( + fetching_interval, + keep_data_for, + await hashApiKey(api_key), + ); + return responseHandler.ok(set, "Updated DockStatAPI config"); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Modifies core API settings including data collection intervals, retention periods, and security credentials", + responses: { + "200": { + description: "Successfully updated configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Updated DockStatAPI config", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error updating configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error updating the DockStatAPI config", + }, + }, + }, + }, + }, + }, + }, + }, + body: t.Object({ + fetching_interval: t.Number(), + keep_data_for: t.Number(), + api_key: t.String(), + }), + }, + ) + .get( + "/package", + async () => { + try { + logger.debug("Fetching package.json"); + const data = { + version: version, + description: description, + license: license, + authorName: authorName, + authorEmail: authorEmail, + authorWebsite: authorWebsite, + contributors: contributors, + dependencies: dependencies, + devDependencies: devDependencies, + }; - logger.debug( - `Received: ${JSON.stringify(data).length} chars in package.json` - ); + logger.debug( + `Received: ${JSON.stringify(data).length} chars in package.json`, + ); - if (JSON.stringify(data).length <= 10) { - throw new Error("Failed to read package.json"); - } + if (JSON.stringify(data).length <= 10) { + throw new Error("Failed to read package.json"); + } - return data; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Displays package metadata including dependencies, contributors, and licensing information", - responses: { - "200": { - description: "Successfully retrieved package information", - content: { - "application/json": { - schema: { - type: "object", - properties: { - version: { - type: "string", - example: "3.0.0", - }, - description: { - type: "string", - example: - "DockStatAPI is an API backend featuring plugins and more for DockStat", - }, - license: { - type: "string", - example: "CC BY-NC 4.0", - }, - authorName: { - type: "string", - example: "ItsNik", - }, - authorEmail: { - type: "string", - example: "info@itsnik.de", - }, - authorWebsite: { - type: "string", - example: "https://github.com/Its4Nik", - }, - contributors: { - type: "array", - items: { - type: "string", - }, - example: [], - }, - dependencies: { - type: "object", - example: { - "@elysiajs/server-timing": "^1.2.1", - "@elysiajs/static": "^1.2.0", - }, - }, - devDependencies: { - type: "object", - example: { - "@biomejs/biome": "1.9.4", - "@types/dockerode": "^3.3.38", - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving package information", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error while reading package.json", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - .post( - "/backup", - async ({ set }) => { - try { - const backupFilename = await dbFunctions.backupDatabase(); - return responseHandler.ok(set, backupFilename); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: "Backs up the internal database", - responses: { - "200": { - description: "Successfully created backup", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "backup_2024-03-20_12-00-00.db.bak", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error creating backup", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error backing up", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - .get( - "/backup", - async ({ set }) => { - try { - const backupFiles = readdirSync(backupDir); + return data; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Displays package metadata including dependencies, contributors, and licensing information", + responses: { + "200": { + description: "Successfully retrieved package information", + content: { + "application/json": { + schema: { + type: "object", + properties: { + version: { + type: "string", + example: "3.0.0", + }, + description: { + type: "string", + example: + "DockStatAPI is an API backend featuring plugins and more for DockStat", + }, + license: { + type: "string", + example: "CC BY-NC 4.0", + }, + authorName: { + type: "string", + example: "ItsNik", + }, + authorEmail: { + type: "string", + example: "info@itsnik.de", + }, + authorWebsite: { + type: "string", + example: "https://github.com/Its4Nik", + }, + contributors: { + type: "array", + items: { + type: "string", + }, + example: [], + }, + dependencies: { + type: "object", + example: { + "@elysiajs/server-timing": "^1.2.1", + "@elysiajs/static": "^1.2.0", + }, + }, + devDependencies: { + type: "object", + example: { + "@biomejs/biome": "1.9.4", + "@types/dockerode": "^3.3.38", + }, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving package information", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error while reading package.json", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) + .post( + "/backup", + async ({ set }) => { + try { + const backupFilename = await dbFunctions.backupDatabase(); + return responseHandler.ok(set, backupFilename); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: "Backs up the internal database", + responses: { + "200": { + description: "Successfully created backup", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "backup_2024-03-20_12-00-00.db.bak", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error creating backup", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error backing up", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) + .get( + "/backup", + async ({ set }) => { + try { + const backupFiles = readdirSync(backupDir); - const filteredFiles = backupFiles.filter((file: string) => { - return !( - file.endsWith(".db") || - file.endsWith(".db-shm") || - file.endsWith(".db-wal") - ); - }); + const filteredFiles = backupFiles.filter((file: string) => { + return !( + file.endsWith(".db") || + file.endsWith(".db-shm") || + file.endsWith(".db-wal") + ); + }); - return filteredFiles; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: "Lists all available backups", - responses: { - "200": { - description: "Successfully retrieved backup list", - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "string", - }, - example: [ - "backup_2024-03-20_12-00-00.db.bak", - "backup_2024-03-19_12-00-00.db.bak", - ], - }, - }, - }, - }, - "400": { - description: "Error retrieving backup list", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Reading Backup directory", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) + return filteredFiles; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: "Lists all available backups", + responses: { + "200": { + description: "Successfully retrieved backup list", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "string", + }, + example: [ + "backup_2024-03-20_12-00-00.db.bak", + "backup_2024-03-19_12-00-00.db.bak", + ], + }, + }, + }, + }, + "400": { + description: "Error retrieving backup list", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Reading Backup directory", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) - .get( - "/backup/download", - async ({ query, set }) => { - try { - const filename = query.filename || dbFunctions.findLatestBackup(); - const filePath = `${backupDir}/${filename}`; + .get( + "/backup/download", + async ({ query, set }) => { + try { + const filename = query.filename || dbFunctions.findLatestBackup(); + const filePath = `${backupDir}/${filename}`; - if (!existsSync(filePath)) { - throw new Error("Backup file not found"); - } + if (!existsSync(filePath)) { + throw new Error("Backup file not found"); + } - set.headers["Content-Type"] = "application/octet-stream"; - set.headers[ - "Content-Disposition" - ] = `attachment; filename="${filename}"`; - return Bun.file(filePath); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Download a specific backup or the latest if no filename is provided", - responses: { - "200": { - description: "Successfully downloaded backup file", - content: { - "application/octet-stream": { - schema: { - type: "string", - format: "binary", - example: "Binary backup file content", - }, - }, - }, - headers: { - "Content-Disposition": { - schema: { - type: "string", - example: - 'attachment; filename="backup_2024-03-20_12-00-00.db.bak"', - }, - }, - }, - }, - "400": { - description: "Error downloading backup", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Backup download failed", - }, - }, - }, - }, - }, - }, - }, - }, - query: t.Object({ - filename: t.Optional(t.String()), - }), - } - ) - .post( - "/restore", - async ({ body, set }) => { - try { - const { file } = body; + set.headers["Content-Type"] = "application/octet-stream"; + set.headers["Content-Disposition"] = + `attachment; filename="${filename}"`; + return Bun.file(filePath); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Download a specific backup or the latest if no filename is provided", + responses: { + "200": { + description: "Successfully downloaded backup file", + content: { + "application/octet-stream": { + schema: { + type: "string", + format: "binary", + example: "Binary backup file content", + }, + }, + }, + headers: { + "Content-Disposition": { + schema: { + type: "string", + example: + 'attachment; filename="backup_2024-03-20_12-00-00.db.bak"', + }, + }, + }, + }, + "400": { + description: "Error downloading backup", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Backup download failed", + }, + }, + }, + }, + }, + }, + }, + }, + query: t.Object({ + filename: t.Optional(t.String()), + }), + }, + ) + .post( + "/restore", + async ({ body, set }) => { + try { + const { file } = body; - set.headers["Content-Type"] = "text/html"; + set.headers["Content-Type"] = "text/html"; - if (!file) { - throw new Error("No file uploaded"); - } + if (!file) { + throw new Error("No file uploaded"); + } - if (!file.name.endsWith(".db.bak")) { - throw new Error("Invalid file type. Expected .db.bak"); - } + if (!file.name.endsWith(".db.bak")) { + throw new Error("Invalid file type. Expected .db.bak"); + } - const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; - const fileBuffer = await file.arrayBuffer(); + const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; + const fileBuffer = await file.arrayBuffer(); - await Bun.write(tempPath, fileBuffer); - dbFunctions.restoreDatabase(tempPath); - unlinkSync(tempPath); + await Bun.write(tempPath, fileBuffer); + dbFunctions.restoreDatabase(tempPath); + unlinkSync(tempPath); - return responseHandler.ok(set, "Database restored successfully"); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - body: t.Object({ file: t.File() }), - detail: { - tags: ["Management"], - description: "Restore database from uploaded backup file", - responses: { - "200": { - description: "Successfully restored database", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Database restored successfully", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error restoring database", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Database restoration error", - }, - }, - }, - }, - }, - }, - }, - }, - } - ); + return responseHandler.ok(set, "Database restored successfully"); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + body: t.Object({ file: t.File() }), + detail: { + tags: ["Management"], + description: "Restore database from uploaded backup file", + responses: { + "200": { + description: "Successfully restored database", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Database restored successfully", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error restoring database", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Database restoration error", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ); diff --git a/src/routes/docker-manager.ts b/src/routes/docker-manager.ts index 279fa2b..fcd877e 100644 --- a/src/routes/docker-manager.ts +++ b/src/routes/docker-manager.ts @@ -1,255 +1,255 @@ import { Elysia, t } from "elysia"; -import type { DockerHost } from "~/typings/docker"; import { dbFunctions } from "~/core/database"; import { logger } from "~/core/utils/logger"; import { responseHandler } from "~/core/utils/response-handler"; +import type { DockerHost } from "~/typings/docker"; export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) - .post( - "/add-host", - async ({ set, body }) => { - try { - dbFunctions.addDockerHost(body as DockerHost); - return responseHandler.ok(set, `Added docker host (${body.name})`); - } catch (error: unknown) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Registers a new Docker host to the monitoring system with connection details", - responses: { - "200": { - description: "Successfully added Docker host", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Added docker host (Localhost)", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error adding Docker host", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error adding docker Host", - }, - }, - }, - }, - }, - }, - }, - }, - body: t.Object({ - name: t.String(), - hostAddress: t.String(), - secure: t.Boolean(), - }), - } - ) + .post( + "/add-host", + async ({ set, body }) => { + try { + dbFunctions.addDockerHost(body as DockerHost); + return responseHandler.ok(set, `Added docker host (${body.name})`); + } catch (error: unknown) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Registers a new Docker host to the monitoring system with connection details", + responses: { + "200": { + description: "Successfully added Docker host", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Added docker host (Localhost)", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error adding Docker host", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error adding docker Host", + }, + }, + }, + }, + }, + }, + }, + }, + body: t.Object({ + name: t.String(), + hostAddress: t.String(), + secure: t.Boolean(), + }), + }, + ) - .post( - "/update-host", - async ({ set, body }) => { - try { - set.status = 200; - dbFunctions.updateDockerHost(body); - return responseHandler.ok(set, `Updated docker host (${body.id})`); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Modifies existing Docker host configuration parameters (name, address, security)", - responses: { - "200": { - description: "Successfully updated Docker host", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Updated docker host (1)", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error updating Docker host", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to update host", - }, - }, - }, - }, - }, - }, - }, - }, - body: t.Object({ - id: t.Number(), - name: t.String(), - hostAddress: t.String(), - secure: t.Boolean(), - }), - } - ) + .post( + "/update-host", + async ({ set, body }) => { + try { + set.status = 200; + dbFunctions.updateDockerHost(body); + return responseHandler.ok(set, `Updated docker host (${body.id})`); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Modifies existing Docker host configuration parameters (name, address, security)", + responses: { + "200": { + description: "Successfully updated Docker host", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Updated docker host (1)", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error updating Docker host", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to update host", + }, + }, + }, + }, + }, + }, + }, + }, + body: t.Object({ + id: t.Number(), + name: t.String(), + hostAddress: t.String(), + secure: t.Boolean(), + }), + }, + ) - .get( - "/hosts", - async ({ set }) => { - try { - const dockerHosts = dbFunctions.getDockerHosts(); + .get( + "/hosts", + async ({ set }) => { + try { + const dockerHosts = dbFunctions.getDockerHosts(); - logger.debug("Retrieved docker hosts"); - return dockerHosts; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Lists all configured Docker hosts with their connection settings", - responses: { - "200": { - description: "Successfully retrieved Docker hosts", - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "object", - properties: { - id: { - type: "number", - example: 1, - }, - name: { - type: "string", - example: "Localhost", - }, - hostAddress: { - type: "string", - example: "localhost:2375", - }, - secure: { - type: "boolean", - example: false, - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving Docker hosts", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve hosts", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) + logger.debug("Retrieved docker hosts"); + return dockerHosts; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Lists all configured Docker hosts with their connection settings", + responses: { + "200": { + description: "Successfully retrieved Docker hosts", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "object", + properties: { + id: { + type: "number", + example: 1, + }, + name: { + type: "string", + example: "Localhost", + }, + hostAddress: { + type: "string", + example: "localhost:2375", + }, + secure: { + type: "boolean", + example: false, + }, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving Docker hosts", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve hosts", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) - .delete( - "/hosts/:id", - async ({ set, params }) => { - try { - set.status = 200; - dbFunctions.deleteDockerHost(params.id); - return responseHandler.ok(set, `Deleted docker host (${params.id})`); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Removes Docker host from monitoring system and clears associated data", - responses: { - "200": { - description: "Successfully deleted Docker host", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Deleted docker host (1)", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error deleting Docker host", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to delete host", - }, - }, - }, - }, - }, - }, - }, - }, - params: t.Object({ - id: t.Number(), - }), - } - ); + .delete( + "/hosts/:id", + async ({ set, params }) => { + try { + set.status = 200; + dbFunctions.deleteDockerHost(params.id); + return responseHandler.ok(set, `Deleted docker host (${params.id})`); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Removes Docker host from monitoring system and clears associated data", + responses: { + "200": { + description: "Successfully deleted Docker host", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Deleted docker host (1)", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error deleting Docker host", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to delete host", + }, + }, + }, + }, + }, + }, + }, + }, + params: t.Object({ + id: t.Number(), + }), + }, + ); diff --git a/src/routes/docker-stats.ts b/src/routes/docker-stats.ts index f9d966d..aa968d2 100644 --- a/src/routes/docker-stats.ts +++ b/src/routes/docker-stats.ts @@ -3,8 +3,8 @@ import { Elysia } from "elysia"; import { dbFunctions } from "~/core/database"; import { getDockerClient } from "~/core/docker/client"; import { - calculateCpuPercent, - calculateMemoryUsage, + calculateCpuPercent, + calculateMemoryUsage, } from "~/core/utils/calculations"; import { findObjectByKey } from "~/core/utils/helpers"; import { logger } from "~/core/utils/logger"; @@ -13,586 +13,586 @@ import type { ContainerInfo, DockerHost, HostStats } from "~/typings/docker"; import type { DockerInfo } from "~/typings/dockerode"; export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) - .get( - "/containers", - async ({ set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const containers: ContainerInfo[] = []; + .get( + "/containers", + async ({ set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + const containers: ContainerInfo[] = []; - await Promise.all( - hosts.map(async (host) => { - try { - const docker = getDockerClient(host); - try { - await docker.ping(); - } catch (pingError) { - return responseHandler.error( - set, - pingError as string, - "Docker host connection failed" - ); - } + await Promise.all( + hosts.map(async (host) => { + try { + const docker = getDockerClient(host); + try { + await docker.ping(); + } catch (pingError) { + return responseHandler.error( + set, + pingError as string, + "Docker host connection failed", + ); + } - const hostContainers = await docker.listContainers({ all: true }); + const hostContainers = await docker.listContainers({ all: true }); - await Promise.all( - hostContainers.map(async (containerInfo) => { - try { - const container = docker.getContainer(containerInfo.Id); - const stats = await new Promise( - (resolve, reject) => { - container.stats({ stream: false }, (error, stats) => { - if (error) { - return responseHandler.reject( - set, - reject, - "An error occurred", - error - ); - } - if (!stats) { - return responseHandler.reject( - set, - reject, - "No stats available" - ); - } - resolve(stats); - }); - } - ); + await Promise.all( + hostContainers.map(async (containerInfo) => { + try { + const container = docker.getContainer(containerInfo.Id); + const stats = await new Promise( + (resolve, reject) => { + container.stats({ stream: false }, (error, stats) => { + if (error) { + return responseHandler.reject( + set, + reject, + "An error occurred", + error, + ); + } + if (!stats) { + return responseHandler.reject( + set, + reject, + "No stats available", + ); + } + resolve(stats); + }); + }, + ); - containers.push({ - id: containerInfo.Id, - hostId: `${host.id}`, - name: containerInfo.Names[0].replace(/^\//, ""), - image: containerInfo.Image, - status: containerInfo.Status, - state: containerInfo.State, - cpuUsage: calculateCpuPercent(stats), - memoryUsage: calculateMemoryUsage(stats), - stats: stats, - info: containerInfo, - }); - } catch (containerError) { - logger.error( - "Error fetching container stats,", - containerError - ); - } - }) - ); - logger.debug(`Fetched stats for ${host.name}`); - } catch (error) { - const errMsg = - error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }) - ); + containers.push({ + id: containerInfo.Id, + hostId: `${host.id}`, + name: containerInfo.Names[0].replace(/^\//, ""), + image: containerInfo.Image, + status: containerInfo.Status, + state: containerInfo.State, + cpuUsage: calculateCpuPercent(stats), + memoryUsage: calculateMemoryUsage(stats), + stats: stats, + info: containerInfo, + }); + } catch (containerError) { + logger.error( + "Error fetching container stats,", + containerError, + ); + } + }), + ); + logger.debug(`Fetched stats for ${host.name}`); + } catch (error) { + const errMsg = + error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }), + ); - logger.debug("Fetched all containers across all hosts"); - return { containers }; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Statistics"], - description: - "Collects real-time statistics for all Docker containers across monitored hosts, including CPU and memory utilization", - responses: { - "200": { - description: "Successfully retrieved container statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - containers: { - type: "array", - items: { - type: "object", - properties: { - id: { - type: "string", - example: "abc123def456", - }, - hostId: { - type: "string", - example: "1", - }, - name: { - type: "string", - example: "example-container", - }, - image: { - type: "string", - example: "nginx:latest", - }, - status: { - type: "string", - example: "running", - }, - state: { - type: "string", - example: "running", - }, - cpuUsage: { - type: "number", - example: 0.5, - }, - memoryUsage: { - type: "number", - example: 1024, - }, - }, - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving container statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve containers", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - .get( - "/hosts", - async ({ set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + logger.debug("Fetched all containers across all hosts"); + return { containers }; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Statistics"], + description: + "Collects real-time statistics for all Docker containers across monitored hosts, including CPU and memory utilization", + responses: { + "200": { + description: "Successfully retrieved container statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + containers: { + type: "array", + items: { + type: "object", + properties: { + id: { + type: "string", + example: "abc123def456", + }, + hostId: { + type: "string", + example: "1", + }, + name: { + type: "string", + example: "example-container", + }, + image: { + type: "string", + example: "nginx:latest", + }, + status: { + type: "string", + example: "running", + }, + state: { + type: "string", + example: "running", + }, + cpuUsage: { + type: "number", + example: 0.5, + }, + memoryUsage: { + type: "number", + example: 1024, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving container statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve containers", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) + .get( + "/hosts", + async ({ set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const stats: HostStats[] = []; + const stats: HostStats[] = []; - for (const host of hosts) { - const docker = getDockerClient(host); - const info: DockerInfo = await docker.info(); + for (const host of hosts) { + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); - const config: HostStats = { - hostId: host.id as number, - hostName: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - labels: info.Labels, - images: info.Images, - containers: info.Containers, - containersPaused: info.ContainersPaused, - containersRunning: info.ContainersRunning, - containersStopped: info.ContainersStopped, - }; + const config: HostStats = { + hostId: host.id as number, + hostName: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; - stats.push(config); - } + stats.push(config); + } - logger.debug("Fetched all hosts"); - return stats; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to retrieve host config" - ); - } - }, - { - detail: { - tags: ["Statistics"], - description: - "Provides detailed system metrics and Docker runtime information for specified host", - responses: { - "200": { - description: "Successfully retrieved host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - hostId: { - type: "number", - example: 1, - }, - hostName: { - type: "string", - example: "Localhost", - }, - dockerVersion: { - type: "string", - example: "24.0.5", - }, - apiVersion: { - type: "string", - example: "1.41", - }, - os: { - type: "string", - example: "Linux", - }, - architecture: { - type: "string", - example: "x86_64", - }, - totalMemory: { - type: "number", - example: 16777216, - }, - totalCPU: { - type: "number", - example: 4, - }, - labels: { - type: "array", - items: { - type: "string", - }, - example: ["environment=production"], - }, - images: { - type: "number", - example: 10, - }, - containers: { - type: "number", - example: 5, - }, - containersPaused: { - type: "number", - example: 0, - }, - containersRunning: { - type: "number", - example: 4, - }, - containersStopped: { - type: "number", - example: 1, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve host config", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - .get( - "/hosts", - async ({ set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + logger.debug("Fetched all hosts"); + return stats; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to retrieve host config", + ); + } + }, + { + detail: { + tags: ["Statistics"], + description: + "Provides detailed system metrics and Docker runtime information for specified host", + responses: { + "200": { + description: "Successfully retrieved host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + hostId: { + type: "number", + example: 1, + }, + hostName: { + type: "string", + example: "Localhost", + }, + dockerVersion: { + type: "string", + example: "24.0.5", + }, + apiVersion: { + type: "string", + example: "1.41", + }, + os: { + type: "string", + example: "Linux", + }, + architecture: { + type: "string", + example: "x86_64", + }, + totalMemory: { + type: "number", + example: 16777216, + }, + totalCPU: { + type: "number", + example: 4, + }, + labels: { + type: "array", + items: { + type: "string", + }, + example: ["environment=production"], + }, + images: { + type: "number", + example: 10, + }, + containers: { + type: "number", + example: 5, + }, + containersPaused: { + type: "number", + example: 0, + }, + containersRunning: { + type: "number", + example: 4, + }, + containersStopped: { + type: "number", + example: 1, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve host config", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) + .get( + "/hosts", + async ({ set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const stats: HostStats[] = []; + const stats: HostStats[] = []; - for (const host of hosts) { - const docker = getDockerClient(host); - const info: DockerInfo = await docker.info(); + for (const host of hosts) { + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); - const config: HostStats = { - hostId: host.id as number, - hostName: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - labels: info.Labels, - images: info.Images, - containers: info.Containers, - containersPaused: info.ContainersPaused, - containersRunning: info.ContainersRunning, - containersStopped: info.ContainersStopped, - }; + const config: HostStats = { + hostId: host.id as number, + hostName: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; - stats.push(config); - } + stats.push(config); + } - logger.debug("Fetched stats for all hosts"); - return stats; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to retrieve host config" - ); - } - }, - { - detail: { - tags: ["Statistics"], - description: - "Provides detailed system metrics and Docker runtime information for all hosts", - responses: { - "200": { - description: "Successfully retrieved host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - hostId: { - type: "number", - example: 1, - }, - hostName: { - type: "string", - example: "Localhost", - }, - dockerVersion: { - type: "string", - example: "24.0.5", - }, - apiVersion: { - type: "string", - example: "1.41", - }, - os: { - type: "string", - example: "Linux", - }, - architecture: { - type: "string", - example: "x86_64", - }, - totalMemory: { - type: "number", - example: 16777216, - }, - totalCPU: { - type: "number", - example: 4, - }, - labels: { - type: "array", - items: { - type: "string", - }, - example: ["environment=production"], - }, - images: { - type: "number", - example: 10, - }, - containers: { - type: "number", - example: 5, - }, - containersPaused: { - type: "number", - example: 0, - }, - containersRunning: { - type: "number", - example: 4, - }, - containersStopped: { - type: "number", - example: 1, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve host config", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - .get( - "/hosts/:id", - async ({ params, set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + logger.debug("Fetched stats for all hosts"); + return stats; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to retrieve host config", + ); + } + }, + { + detail: { + tags: ["Statistics"], + description: + "Provides detailed system metrics and Docker runtime information for all hosts", + responses: { + "200": { + description: "Successfully retrieved host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + hostId: { + type: "number", + example: 1, + }, + hostName: { + type: "string", + example: "Localhost", + }, + dockerVersion: { + type: "string", + example: "24.0.5", + }, + apiVersion: { + type: "string", + example: "1.41", + }, + os: { + type: "string", + example: "Linux", + }, + architecture: { + type: "string", + example: "x86_64", + }, + totalMemory: { + type: "number", + example: 16777216, + }, + totalCPU: { + type: "number", + example: 4, + }, + labels: { + type: "array", + items: { + type: "string", + }, + example: ["environment=production"], + }, + images: { + type: "number", + example: 10, + }, + containers: { + type: "number", + example: 5, + }, + containersPaused: { + type: "number", + example: 0, + }, + containersRunning: { + type: "number", + example: 4, + }, + containersStopped: { + type: "number", + example: 1, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve host config", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) + .get( + "/hosts/:id", + async ({ params, set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const host = findObjectByKey(hosts, "id", Number(params.id)); - if (!host) { - return responseHandler.simple_error( - set, - `Host (${params.id}) not found` - ); - } + const host = findObjectByKey(hosts, "id", Number(params.id)); + if (!host) { + return responseHandler.simple_error( + set, + `Host (${params.id}) not found`, + ); + } - const docker = getDockerClient(host); - const info: DockerInfo = await docker.info(); + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); - const config: HostStats = { - hostId: host.id as number, - hostName: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - labels: info.Labels, - images: info.Images, - containers: info.Containers, - containersPaused: info.ContainersPaused, - containersRunning: info.ContainersRunning, - containersStopped: info.ContainersStopped, - }; + const config: HostStats = { + hostId: host.id as number, + hostName: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; - logger.debug(`Fetched config for ${host.name}`); - return config; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to retrieve host config" - ); - } - }, - { - detail: { - tags: ["Statistics"], - description: - "Provides detailed system metrics and Docker runtime information for specified host", - responses: { - "200": { - description: "Successfully retrieved host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - hostId: { - type: "number", - example: 1, - }, - hostName: { - type: "string", - example: "Localhost", - }, - dockerVersion: { - type: "string", - example: "24.0.5", - }, - apiVersion: { - type: "string", - example: "1.41", - }, - os: { - type: "string", - example: "Linux", - }, - architecture: { - type: "string", - example: "x86_64", - }, - totalMemory: { - type: "number", - example: 16777216, - }, - totalCPU: { - type: "number", - example: 4, - }, - labels: { - type: "array", - items: { - type: "string", - }, - example: ["environment=production"], - }, - images: { - type: "number", - example: 10, - }, - containers: { - type: "number", - example: 5, - }, - containersPaused: { - type: "number", - example: 0, - }, - containersRunning: { - type: "number", - example: 4, - }, - containersStopped: { - type: "number", - example: 1, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve host config", - }, - }, - }, - }, - }, - }, - }, - }, - } - ); + logger.debug(`Fetched config for ${host.name}`); + return config; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to retrieve host config", + ); + } + }, + { + detail: { + tags: ["Statistics"], + description: + "Provides detailed system metrics and Docker runtime information for specified host", + responses: { + "200": { + description: "Successfully retrieved host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + hostId: { + type: "number", + example: 1, + }, + hostName: { + type: "string", + example: "Localhost", + }, + dockerVersion: { + type: "string", + example: "24.0.5", + }, + apiVersion: { + type: "string", + example: "1.41", + }, + os: { + type: "string", + example: "Linux", + }, + architecture: { + type: "string", + example: "x86_64", + }, + totalMemory: { + type: "number", + example: 16777216, + }, + totalCPU: { + type: "number", + example: 4, + }, + labels: { + type: "array", + items: { + type: "string", + }, + example: ["environment=production"], + }, + images: { + type: "number", + example: 10, + }, + containers: { + type: "number", + example: 5, + }, + containersPaused: { + type: "number", + example: 0, + }, + containersRunning: { + type: "number", + example: 4, + }, + containersStopped: { + type: "number", + example: 1, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve host config", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ); diff --git a/src/tests/docker-manager.spec.ts b/src/tests/docker-manager.spec.ts index 64e04d2..e1b52d7 100644 --- a/src/tests/docker-manager.spec.ts +++ b/src/tests/docker-manager.spec.ts @@ -3,462 +3,462 @@ import { Elysia } from "elysia"; import { dbFunctions } from "~/core/database"; import { dockerRoutes } from "~/routes/docker-manager"; import { - generateMarkdownReport, - recordTestResult, - testResults, + generateMarkdownReport, + recordTestResult, + testResults, } from "./markdown-exporter"; import type { TestContext } from "./markdown-exporter"; type DockerHost = { - id?: number; - name: string; - hostAddress: string; - secure: boolean; + id?: number; + name: string; + hostAddress: string; + secure: boolean; }; const mockDb = { - addDockerHost: mock(() => ({ - changes: 1, - lastInsertRowid: 1, - })), - updateDockerHost: mock(() => ({ - changes: 1, - lastInsertRowid: 1, - })), - getDockerHosts: mock(() => []), - deleteDockerHost: mock(() => ({ - changes: 1, - lastInsertRowid: 1, - })), + addDockerHost: mock(() => ({ + changes: 1, + lastInsertRowid: 1, + })), + updateDockerHost: mock(() => ({ + changes: 1, + lastInsertRowid: 1, + })), + getDockerHosts: mock(() => []), + deleteDockerHost: mock(() => ({ + changes: 1, + lastInsertRowid: 1, + })), }; mock.module("~/core/database", () => ({ - dbFunctions: mockDb, + dbFunctions: mockDb, })); mock.module("~/core/utils/logger", () => ({ - logger: { - debug: mock(), - info: mock(), - error: mock(), - }, + logger: { + debug: mock(), + info: mock(), + error: mock(), + }, })); const createApp = () => new Elysia().use(dockerRoutes).decorate({}); async function captureTestContext( - req: Request, - res: Response + req: Request, + res: Response, ): Promise { - const responseStatus = res.status; - const responseHeaders = Object.fromEntries(res.headers.entries()); - let responseBody: unknown; - - try { - responseBody = await res.clone().json(); - } catch (parseError) { - try { - responseBody = await res.clone().text(); - } catch { - responseBody = "Unparseable response content"; - } - } - - return { - request: { - method: req.method, - url: req.url, - headers: Object.fromEntries(req.headers.entries()), - body: req.body ? await req.clone().text() : undefined, - }, - response: { - status: responseStatus, - headers: responseHeaders, - body: responseBody, - }, - }; + const responseStatus = res.status; + const responseHeaders = Object.fromEntries(res.headers.entries()); + let responseBody: unknown; + + try { + responseBody = await res.clone().json(); + } catch (parseError) { + try { + responseBody = await res.clone().text(); + } catch { + responseBody = "Unparseable response content"; + } + } + + return { + request: { + method: req.method, + url: req.url, + headers: Object.fromEntries(req.headers.entries()), + body: req.body ? await req.clone().text() : undefined, + }, + response: { + status: responseStatus, + headers: responseHeaders, + body: responseBody, + }, + }; } describe("Docker Configuration Endpoints", () => { - beforeEach(() => { - mockDb.addDockerHost.mockClear(); - mockDb.updateDockerHost.mockClear(); - mockDb.getDockerHosts.mockClear(); - mockDb.deleteDockerHost.mockClear(); - }); - - describe("POST /docker-config/add-host", () => { - it("should add a docker host successfully", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const host: DockerHost = { - name: "Host1", - hostAddress: "127.0.0.1:2375", - secure: false, - }; - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/add-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - message: `Added docker host (${host.name})`, - }); - expect(mockDb.addDockerHost).toHaveBeenCalledWith(host); - - recordTestResult({ - name: "add-host success", - suite: "Docker Config - Add Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "add-host success", - suite: "Docker Config - Add Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with success message", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle error when adding a docker host fails", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const host: DockerHost = { - name: "Host2", - hostAddress: "invalid", - secure: true, - }; - - // Set mock implementation - mockDb.addDockerHost.mockImplementationOnce(() => { - throw new Error("DB error"); - }); - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/add-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - message: expect.any(String), - }); - - recordTestResult({ - name: "add-host failure", - suite: "Docker Config - Add Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "add-host failure", - suite: "Docker Config - Add Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "400 Error with error structure", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("POST /docker-config/update-host", () => { - it("should update a docker host successfully", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const host: DockerHost = { - id: 1, - name: "Host1-upd", - hostAddress: "127.0.0.1:2376", - secure: true, - }; - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/update-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - message: `Updated docker host (${host.id})`, - }); - expect(mockDb.updateDockerHost).toHaveBeenCalledWith(host); - - recordTestResult({ - name: "update-host success", - suite: "Docker Config - Update Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "update-host success", - suite: "Docker Config - Update Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with update confirmation", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle error when update fails", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const host: DockerHost = { - id: 2, - name: "Host2", - hostAddress: "x", - secure: false, - }; - - mockDb.updateDockerHost.mockImplementationOnce(() => { - throw new Error("Update error"); - }); - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/update-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - message: expect.any(String), - }); - - recordTestResult({ - name: "update-host failure", - suite: "Docker Config - Update Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "update-host failure", - suite: "Docker Config - Update Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "400 Error with error details", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("GET /docker-config/hosts", () => { - it("should retrieve list of hosts", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const hosts: DockerHost[] = [ - { id: 1, name: "H1", hostAddress: "a", secure: false }, - ]; - - mockDb.getDockerHosts.mockImplementation(() => hosts as never[]); - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/hosts"); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toEqual(hosts); - - recordTestResult({ - name: "get-hosts success", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "get-hosts success", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with hosts array", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle error when retrieval fails", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - mockDb.getDockerHosts.mockImplementationOnce(() => { - throw new Error("Fetch error"); - }); - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/hosts"); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - message: expect.any(String), - }); - - recordTestResult({ - name: "get-hosts failure", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "get-hosts failure", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "400 Error with error details", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("DELETE /docker-config/hosts/:id", () => { - it("should delete a host successfully", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const id = 5; - - try { - const app = createApp(); - const req = new Request(`http://localhost/docker-config/hosts/${id}`, { - method: "DELETE", - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - message: `Deleted docker host (${id})`, - }); - expect(mockDb.deleteDockerHost).toHaveBeenCalledWith(id); - - recordTestResult({ - name: "delete-host success", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "delete-host success", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with deletion confirmation", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle error when delete fails", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const id = 6; - - mockDb.deleteDockerHost.mockImplementationOnce(() => { - throw new Error("Delete error"); - }); - - try { - const app = createApp(); - const req = new Request(`http://localhost/docker-config/hosts/${id}`, { - method: "DELETE", - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - message: expect.any(String), - }); - - recordTestResult({ - name: "delete-host failure", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "delete-host failure", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "400 Error with error details", - received: context?.response, - }, - }); - throw error; - } - }); - }); + beforeEach(() => { + mockDb.addDockerHost.mockClear(); + mockDb.updateDockerHost.mockClear(); + mockDb.getDockerHosts.mockClear(); + mockDb.deleteDockerHost.mockClear(); + }); + + describe("POST /docker-config/add-host", () => { + it("should add a docker host successfully", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + name: "Host1", + hostAddress: "127.0.0.1:2375", + secure: false, + }; + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/add-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + message: `Added docker host (${host.name})`, + }); + expect(mockDb.addDockerHost).toHaveBeenCalledWith(host); + + recordTestResult({ + name: "add-host success", + suite: "Docker Config - Add Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "add-host success", + suite: "Docker Config - Add Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with success message", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when adding a docker host fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + name: "Host2", + hostAddress: "invalid", + secure: true, + }; + + // Set mock implementation + mockDb.addDockerHost.mockImplementationOnce(() => { + throw new Error("DB error"); + }); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/add-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + message: expect.any(String), + }); + + recordTestResult({ + name: "add-host failure", + suite: "Docker Config - Add Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "add-host failure", + suite: "Docker Config - Add Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error structure", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("POST /docker-config/update-host", () => { + it("should update a docker host successfully", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + id: 1, + name: "Host1-upd", + hostAddress: "127.0.0.1:2376", + secure: true, + }; + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/update-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + message: `Updated docker host (${host.id})`, + }); + expect(mockDb.updateDockerHost).toHaveBeenCalledWith(host); + + recordTestResult({ + name: "update-host success", + suite: "Docker Config - Update Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "update-host success", + suite: "Docker Config - Update Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with update confirmation", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when update fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + id: 2, + name: "Host2", + hostAddress: "x", + secure: false, + }; + + mockDb.updateDockerHost.mockImplementationOnce(() => { + throw new Error("Update error"); + }); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/update-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + message: expect.any(String), + }); + + recordTestResult({ + name: "update-host failure", + suite: "Docker Config - Update Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "update-host failure", + suite: "Docker Config - Update Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error details", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("GET /docker-config/hosts", () => { + it("should retrieve list of hosts", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const hosts: DockerHost[] = [ + { id: 1, name: "H1", hostAddress: "a", secure: false }, + ]; + + mockDb.getDockerHosts.mockImplementation(() => hosts as never[]); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/hosts"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toEqual(hosts); + + recordTestResult({ + name: "get-hosts success", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "get-hosts success", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with hosts array", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when retrieval fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + mockDb.getDockerHosts.mockImplementationOnce(() => { + throw new Error("Fetch error"); + }); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/hosts"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + message: expect.any(String), + }); + + recordTestResult({ + name: "get-hosts failure", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "get-hosts failure", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error details", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("DELETE /docker-config/hosts/:id", () => { + it("should delete a host successfully", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const id = 5; + + try { + const app = createApp(); + const req = new Request(`http://localhost/docker-config/hosts/${id}`, { + method: "DELETE", + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + message: `Deleted docker host (${id})`, + }); + expect(mockDb.deleteDockerHost).toHaveBeenCalledWith(id); + + recordTestResult({ + name: "delete-host success", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "delete-host success", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with deletion confirmation", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when delete fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const id = 6; + + mockDb.deleteDockerHost.mockImplementationOnce(() => { + throw new Error("Delete error"); + }); + + try { + const app = createApp(); + const req = new Request(`http://localhost/docker-config/hosts/${id}`, { + method: "DELETE", + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + message: expect.any(String), + }); + + recordTestResult({ + name: "delete-host failure", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "delete-host failure", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error details", + received: context?.response, + }, + }); + throw error; + } + }); + }); }); afterAll(() => { - generateMarkdownReport(); + generateMarkdownReport(); }); From 2e8528ab8a500a925f3411582a21fbeccc683262 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Fri, 16 May 2025 10:45:58 +0200 Subject: [PATCH 324/324] UT: Fix wrong body selection --- src/tests/docker-manager.spec.ts | 92 +++++++++++++++++++------------- 1 file changed, 55 insertions(+), 37 deletions(-) diff --git a/src/tests/docker-manager.spec.ts b/src/tests/docker-manager.spec.ts index 64e04d2..3c64350 100644 --- a/src/tests/docker-manager.spec.ts +++ b/src/tests/docker-manager.spec.ts @@ -99,11 +99,14 @@ describe("Docker Configuration Endpoints", () => { try { const app = createApp(); - const req = new Request("http://localhost/docker-config/add-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); + const req = new Request( + "http://localhost:3000/docker-config/add-host", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + } + ); const res = await app.handle(req); context = await captureTestContext(req, res); @@ -146,22 +149,25 @@ describe("Docker Configuration Endpoints", () => { // Set mock implementation mockDb.addDockerHost.mockImplementationOnce(() => { - throw new Error("DB error"); + throw new Error("Mock Database Error"); }); try { const app = createApp(); - const req = new Request("http://localhost/docker-config/add-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); + const req = new Request( + "http://localhost:3000/docker-config/add-host", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + } + ); const res = await app.handle(req); context = await captureTestContext(req, res); expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - message: expect.any(String), + expect(context.response).toMatchObject({ + body: expect.any(String), }); recordTestResult({ @@ -200,11 +206,14 @@ describe("Docker Configuration Endpoints", () => { try { const app = createApp(); - const req = new Request("http://localhost/docker-config/update-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); + const req = new Request( + "http://localhost:3000/docker-config/update-host", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + } + ); const res = await app.handle(req); context = await captureTestContext(req, res); @@ -252,17 +261,20 @@ describe("Docker Configuration Endpoints", () => { try { const app = createApp(); - const req = new Request("http://localhost/docker-config/update-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); + const req = new Request( + "http://localhost:3000/docker-config/update-host", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + } + ); const res = await app.handle(req); context = await captureTestContext(req, res); expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - message: expect.any(String), + expect(context.response).toMatchObject({ + body: expect.any(String), }); recordTestResult({ @@ -300,7 +312,7 @@ describe("Docker Configuration Endpoints", () => { try { const app = createApp(); - const req = new Request("http://localhost/docker-config/hosts"); + const req = new Request("http://localhost:3000/docker-config/hosts"); const res = await app.handle(req); context = await captureTestContext(req, res); @@ -339,13 +351,13 @@ describe("Docker Configuration Endpoints", () => { try { const app = createApp(); - const req = new Request("http://localhost/docker-config/hosts"); + const req = new Request("http://localhost:3000/docker-config/hosts"); const res = await app.handle(req); context = await captureTestContext(req, res); expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - message: expect.any(String), + expect(context.response).toMatchObject({ + body: expect.any(String), }); recordTestResult({ @@ -379,9 +391,12 @@ describe("Docker Configuration Endpoints", () => { try { const app = createApp(); - const req = new Request(`http://localhost/docker-config/hosts/${id}`, { - method: "DELETE", - }); + const req = new Request( + `http://localhost:3000/docker-config/hosts/${id}`, + { + method: "DELETE", + } + ); const res = await app.handle(req); context = await captureTestContext(req, res); @@ -424,15 +439,18 @@ describe("Docker Configuration Endpoints", () => { try { const app = createApp(); - const req = new Request(`http://localhost/docker-config/hosts/${id}`, { - method: "DELETE", - }); + const req = new Request( + `http://localhost:3000/docker-config/hosts/${id}`, + { + method: "DELETE", + } + ); const res = await app.handle(req); context = await captureTestContext(req, res); expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - message: expect.any(String), + expect(context.response).toMatchObject({ + body: expect.any(String), }); recordTestResult({