From b0b5b161bcfe8b30ef95ce806d7e2778eb02ccb4 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sun, 23 Feb 2025 09:06:51 +0100 Subject: [PATCH 01/29] 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 6381947a..00000000 --- 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 zwpsr~Ae81C5pC2_*WkAW=<-ur2=$vVD-tAE(vTAO>g{o*J03&j^ix@18=3sa2O~m` z6q{$0XfLy@NUrQ&PR*8jBOVm4YW?#SPOiInpAzqmzVU z6a+GVxrlsRS3j_bPe8gRYb-R&eLdMLE+cP=B*J-9FT?tjp6pm4b?}qHxpWov97U`2 z(i@mhRr)m6cj#fX3V)3GB!>~aM`EZTxzpNGT>pcqn6U-h^O$_f3pF2}1?UW-#~10! zv0qa1-YE0EX6HaLtO5IFMl9RT&W)3=`Uh=&r3w=`xulAEd)jh|C0gALK9qb1xihwQ zXZ0h{mS>E}Xy0Mz54rCyY@^;;7Q<^#o<&f1FxXJfd=%Z`RUVWPa{aVx*g-qUkN!Ji z&N<9*-b5iy)I$i-h)8tdG}(MpAP_5(mz%3Xn9=a_gol|-uStc$+SG@QFd)_|PbKl# zBe1mLR|H!Hd4`iw{_K=gC8(WvUQk)!xnztf6GUY5536faOaz>V9M71-7~ggt33IxzQR0M!f>FnSKL~N z$0AhbB}?Ha`1!ATGU9kb=G};w9|`(0(^n#QQ^@ zLk8Z>By>pj?qg^wVMlEX?&pJ~rJZ+uq$ma0;)Sc8J}S&_m>6~N(pAg6MO1Xb{dR9U zytMja+KPk$ri00qMALwHnzW^vB*k4Mk7v9-kkrRQ{3OF)$u7r9DzzU!p1MBEX3~jN z#&AW&*6MQc@@m$_E~|Ihb7yZl4k6S7aj}q*Jsdm--O^pg$u53 z#a+IiRLR=coLxyCId?6r)+Y9&>>ox;)md!gr0gPn169K}TD1LFElJGWx-5?g$J8J0 zJ1j5{6%esKxJLRxAm4~JOb13(2ld}i8!6+Pu6p+Wz5jdl%7+4SY|Q4%OWQhlRA7^P zAg-+wd@PLr7p)s$WtKo$^(blU(KPDq1=CQ$82?p4Wi$x`kZ5~`|G`G53t&-`cl+F; z3-cOp5)6FZkcv$C-`q5X5cg6nh82`%!Q85L7>5en|M!`Z zjln8Rr$&wVWV4Y#oq`x(O|CC7Obyf6h{|Z?rACwlpR~}Q4p{(}#Ff!;i_EO&u?{C8U!Y1K*2?gdjSo-Q>0x6vV?Y!&yEC%1jZ1AYNJW2^Bm+d)Et z*(%tt4cMg?Kvpn&AOQYc0S@1whpU;W8+?Y!lXku!3U+-3@PWH|98<|CYCcabK#jf> z|3Td^EJ@CGX5G=!LQyS$N*Tbzet^n>w77>9HkOH~G(0W)@7V^xD#+Xx+_qAmL&LuV zLtF}BQUwJzG~ijkh)UnpfAd>YPV|8OnO}!HU0PA=zxhF$ElFbk%vWw!<1Z}2c_tIPemyaZg|!2SVg!lS$d=GP`{bjp>0-g&Y zupT7YaxowIj-$E!t_92bbInm`ESUHn8A5^_huqP;|F46C_*;??zUG*+Xt9>#`wA>w zrKKX!6q@~iXC>=@e!SD}j-RNV^r4F3MUJxnzH|*ZXm}GtSw=3;E?2}Ey+M*K3o|?V zkE$bqp#lwLu;HcjHr=If%#Po^%G0yILQk$P4N5PkQ$rKZd-Y@2g96_actf%zqJRFj z4}ZU;a%FHbM5y91WOlv&{eSE40&At@HE4FoQQm0jmay^itCYIzXMA{G=H5PLBe5ps z2K0@-{^g#g(pSS)RJ_dDzl~ljgxQpEOl7yH)@BR1j_eOqy>-8I(y7@%QxkWwqb~D} zuJ|?gedDrUDw3R~=}3q6^5XAq$#G_Mkb`rF_xRr9^*8yxFO=TOGhtVA_%iNJ*+;AD znA=rx7Q=@rS10Y1!nB`zk%WK0w_gCWSC^)y<$T_>XQ zV*MZOMuoFEY5`3{+ai_TN;e_&II_}L)R{1Io7B z|LVu%2>$5gvH!$&t0l?NB2+{^WB?iCCHg=kE9kujmVaNB<;)J!UTJASLpVD+{@=qx zmL%>ElD7CKD}CcnyqnCN{pixRPK|Xquhxn^mI(f6`~s;amKWU==XU-yZ=c)$lzp8%Fly}$n)%XOvnNC}v%A@W z?LoWdTEpzb-(ihE2L#sN5$C#qEkhIpxSQa4&H!@J@Nw7kx@oXg2XVY97CHt=Td_>f ztI7*(^noetGkz_ZLfAA%icb7fYtOBjnV;?XyK5M3(Ix}TGO%rtpF7y391l)kT+$D* zt&VrF*8Oz({YEO%K;11jqTTsp@DX$6Un?RHc5OsI=`}~6IBd9Bz4b#3R(^)$Raz>9 z+W;&5X$ugrvcu4p%=dqeu<_&I2^|a1d|)KSV3lQwWyY~tj&d1W=Wu*I(-RM#_S$j{ z!|00ppsQxvXO|*Tp`YKBvtC!gOFM@nzOrQXn1{?p-qlzVB=#I19qYQM0}1#I##RRA z!+%>T-l{y=Lj5r*(*SIg^YLl9@{)acxqqpwFGaxf(hd_NFAlr3N^zGvscNFNLN_3C zbow-W?mOYA8+p&NURw|Q4<2YM&3401XU|uiiJyEI!i3O9MsL`yZWbgDAT@2A{jx>% zZ43Bfe0=q3$lOG9YXtdTvpad39f@m>Fz8=STBex-`@*XOMhymW^lxT7rRf;2=S|gE zybzAfIKzPi0AmQa3m^J3SO1KA^2u{L&58+t`UM>m zS_C!bQ3B1kUHs_`P^CT;=#X#WY&(Nl=IZe$QwWeEJqfe-B>!O08t$2!ZD98x|1%}k zPyiu2^X|(1?;gvI9AGSfVag<}U7X9p=1k=kZv>JlcS1}cw03Jd4P2Tx&dod!z zM{rzJ4H*h5RE~2|H^@%6|6?5DQ}ehxcJB5eN5>jD%0ocL%r7>V-|2I+77Qi8*hY!L zu2TAE+EW3+N{xfvwlg-poi($e(X%QBQ&!p*b`{>he?Qock*k^6@YD;eAvy>tZneHn zh!j5VfbIkWF8ISG1iM^bKaM8iO^JYtSoa+kl1n@)THaCgG%ZF2iZ}9JX(0dXHpK5X z`!7vPBKL!t^Jxe~5HO`a%J4s?l*{gB1hls1Z3C!q&%gC)(^WC5f?yM9PLAE2O<*;; zz!Ue=)771Cvx~VN4Z05gbO+~pWJy8;6xKgkDQ8PvPIqqRc`kFcnZHa{mRc8SowZ4W zg!uimpa53N4M-bpC+sJ@r?q7x4K7NZ5{M1~uE%*UMVZlKs}&Sh|DvJ!<)NKVM%YF> znsxo79OY*A#&S=}VeiRIM57qW`@C)97`<*5m(2m3fL7M)SarqkCu~2Q+q;?}e6~V} zPAi)r`0N1kA<~TQ<>k}m^FA?gGcE4et1Xj|L9#N9&CYKseLM7SI99n8xIxPp-^lr zA|{xVWmf`aSA4V!tmk~=g0a_vjIygVvqO2Z!KMr_L!kLrdlaTM)|(U^N_Xj~r73j>T4VBP0uTw% zMU5>e6Vl-MHpVmrD&>P`%B&&ad>MI^%sYEpCl zFo%TUsZPz1&o~4lOv4yJ@&hm{Bv6B0fsg>X!5Xkwt^cHY*YoMLJ;@{xtMHBG?mIN= zR`k-E&*0ptYUw82d=4xmvder`o zYZc2o{%+E96CM#8OHW?ET!jBsV>b?@ftbHu>NhxMg_k`%2UxX(WiT64QiClYwLj`d zBU9xS7KJDILC$842c@HpRHuf`B7fNK7k4zdHkodiL|1VZ1cDK4S!~=CoA_Yx_bter zSd}$NWa9*efPW*^-PPJ!sYoy06P`XdCUQa+uZ{=jFGyqgtK$U<14~ZtYU=6}D}ezz zrKDWxg~LDlFe%_dz6iW$bNrtUWLZ5wbbRG`F515++5N!4geH+c>8#jO>r=6lpjWsnaS+?JB`f2z5kc%w>1L8V-T|HfL95Ns>7c5?*GM3pDM?z=<^A zw#=4pa`Q&IG7{p7+%KP^2Pq@=@T`n6EkVufOF(R)VokB_ma}nMc#zY<0vMNFDmySe zq>$pR-5%eL>v);rV@W{0)<&Dnr*jjM7SGw0lHI@TJ3M85DK30< zMn+6^1JZW^4BoQa=8Z1LyMW=0sr-iqEs;mI{@JZ#1x@^?+4U( z?^~ZcEDi7;MP>5>`;9FG$qtIlLIoVX>@wQ>E0A>{n_xvgrGg)`C6hUU7ND6p2qx*a z`g^(eTzWQC<10#a2V;mkmSKbW5O^BqIm%NqSgTLSD9q2y<}$p1(`mbxck@+tWTTCp zzz2IG_Fsg~iP#Xi-#7Z0{lL7B24864`**-jHE9W8_?wMz2T&2#r(Su@ncI?ojm@am z#}VyW$Y#J?T5ghll#ba$rtb0w@=8W{4|j1rx+zsmzsR@yT^(^xEv2 zBOhX~^(^hT9A%&3(c}i$L~)r>dK!U#W+CAn%@JiieMzJ%1ac`jR)?csfde+EPjZS~4l1T0=3Q(j-;!I1<$W+&}i7sorWrS5Vx@I~} z@qzsJ<3M=UNm+j&KYb|>Zt7XDHAQ`^Tz-r`B%gvnJjbaoUYRe>0HFkWqh48%^ZEf34LA5Xh$C<1#Wiq;(hF>tJO z?}zXxSP`Mpy*`OXG+RiZgQb^T3m=}$=e*pp?#U;OYu=^(1JC=jko-L4HmNyRlB%~a zq^q;OY|&Nme%fD-*yxzxcP@hX#PCEA z0Ye;ePMZn4j;S8sErLK|G=LQQr?DqQXuBUXH?Mi;c~bJ1OqWVUj&h-NCjzjPl%S*7 zw?rz?At4G@^c5>y$gd#5b5=Ib=9p0VjtD}A-2yWJwlEvTACHmtUf+rIr>HLuhkRdx zY{??vzUN)q+t2K}u*A&lN`-jGn!aAU8`p6k2Mpn0Tr42a+R#3X3zXi$fZV(e5uu6o z^DzTQ^+3E8NT@#s$g8^hm}a*_D}hB?!}09wRoIT9l7iuEEJ9FC_==CTp^P~u>`>!4 zyWt@)ik2#gKPJtaN@yy-xXYYbT;_(}%%(!l{!5ok{}meIOotxz#b;Gk@Cxsf*RcXmIo8SSJwEUQC}7iFubG0LHze!% zRM7$otHe|e-CoLFt7}r|x2E!>Ocg>8&A@v7PE6wdd$Fg<{MCS`^_qsa+PN%@BF_*- z>?^sVjtSdWjkv^~Z#?T!Shy*$}H2gtKkr;}!_fm@?EH87i$BFV-y?Ti!asWq3|{uOK9bC0jL z?ZX-U#<7Xg^BNtF)}E@joPCE6TnzHA?yAcu)EAf0-yG$*4%>S5YLHli{1aP44)0Qcq)>`1_bnL%cau~~S zK4nLiS)Mt^Tu4$lvF&`Fx3(TUH$0O5VX|Tmn4_k?d7Vejra;@yb8z`v`Q*4N)mT6> zm~-zkMxK{*KU++q3ozvgo#T&;&PCD6ou4aR3cHFFo8WW<#g~zLL|E^`QL;1b!OV;t zsNZ?`E6nJ34_D-m2}YSr)!@fVyJ#axR+EWK+Sr7%#Bq1WgP&3Z=gv#xxW`>St(-Bl zgH7b~n6fmWWsJBzZ*pf+BPFJ+4)Vo@-eQIsQ+G0L`fa}ojih!==mIB7 zK;6zH9)D#=P}35bULXb&U70@SlAvi9_2ghi4%Sy38seP`65<tBhj-|{~uG5l+x`;-Z^2y4J08trA41xu3hfW5o-X861=+wk2%}2i;tas?J zE;np;)4xx%-)5iDaLc4mYsO+yd*HY2Zi#X2s*(o1AM&obG|?E+6X`I zr1X@`<%+!$P`%y0P8T}zd69T_k&=Do{N^YwQ?Bi+=%Nb}g<)|W(Inrcwh?zGrb#46 zp9?*$!9mE&9Vx4R*fj3EB+>ZO6P`J0CEon1VAOdDf%5dKH-i~Y8IU{n+n4&MOxFP^ zm4c0ICDx&WSqiEc290@o$)l$awhz6en83ML&MFWM2aSOJ{ zSTm8Ntlj8^eBUaM#YUn!iws8UX#O?`R+5S)@c05801Xctkba$)e7@9*y7+`r;4_pL zzP7m2@s(FlJ3Q(v6{+#27)DR&QJa8MJjtVL&imMk%`BBCHOm@Uei$>Nui#lmFan$UxBmh@vZ{zT*gzdbBuxVF_1z7 z?k#(t1&a=M^j>XG^3#150`Px$*6d_WjRU%SCJ-M|g5#KasP9sA;rUwho| zj^=LSRae!zN+RgMXQ&x`Y)7~%V@ofVj2~ZI_A)Wh>d{^GjIO~Abwwhg_du*z141N06W}2F9+@%u7}DWm#E%erjKY%`4~GRxNTbrb--V6dBK%=w?i zJ0`}-_X5O{y^I`|pHzdb?H@VeWfrKVA;SO)tmp^Lg&6F)RU|tIR3v}YSte=$TjBAF z=6*Vjav9Fq843HZ{^BwbkQd&n_OplgNF#3^1AEqOeTQ1m=8^^SGFW0a{Op9D{Z}(s zaz!6ryRP)LIQZ(0j~!-fSKa_a%Zy%(sBF#<^)e-WV?w&fUtM4`4uY%_Vn>E;_z#55 zG%i2B%eZmWsXn6_RIl?|-UiidRint+nMo*}<5)G1lX%=#H{gC7-&B(N=&1r^R0J`o z>xrKji5*~Whq412e(&e*@kR!BjU#o9BfX+YFR{Xx=w&)}^tki+d5r?59_a9#k@|d7 z8uQ*!!OraJ^5`SB{okR>Q*C1E=!Mn?w{}F;lg<%*)Ur|61!(#`G16Xe!TaFa z#|O}2NO6x;h)!)h=va}Zi$OK5Hwh)rrns-o0mP$PePrdBN4z_*_R6lUz1c~=TEuuy z)tVWI_k5d}!}2S%4X#6SrRLlQE-dq1g)wX@3gD7AS0}4osT!7Njl1eKB0xF<% zH_{!7!XPE0)X*SEND4|LjiiK>bV^D$>|^l0p67nwANC)xH$V6h*33HBd7MXlk6f)x z_f|2aWUF?gJG;*dn9e7={w_xlwwW;Tp^x^4*{#(T)(Q_Y)doMt-Tp*py^A$ zy_Z{#AYTxU2y;NRrPPj+hS*EPj~1l#MGxt?6Q0oJ+1vw>8Kocc5=biw9BwW$`>`_s zho0?pL*h>ln)<}?)~r#cIim1tppWilmu>7udx#MeM_ZIS$ z+=4ltHQK#=fv+%fG%dHjxLmuy)waPtbehc?y{aToKrLWLFI5DTr~Hw*dM20W=eoEP zs-?7tuSMQLHAI5HDO~Y+sc`?-oc+49RG)_cQjRIs?aIZaQF~YWojru%*7 zXmvuuR%VW&IfPH^tCyKGB#cpj%>A4b`+UG zr18~!bjL9#qqyKx92;<51e8?AjDugd%lCQ!HW%r<-^MRq5nn8%P%YrpP(S5+S#Cwk z?4aCc!2&MR2;_@~w|a~V;`if0>5Y=Krw=Fish!epyV1YTc`TG9t>L{)ZZ=JC*4}XP=;1nh0;D$DPbGqMX&2PL-tET}xi%0sZt z)~y`^D%J*){*T{r=(=_(w&K2*>h$-%!yEt1XTG0yP~2zb_cx;!u9IdTO~*MMwqjF^ z@>o+xod?l`-!FS5Enx||=9xVt)2)lk=0gnYuSXB`gIPB_A4SoMt0<#+$mST=fdx}-bs(tm-I#pD{bZ~G+!aoV`+jAOff1jt zTK+C|Lz8rOX51H;P=@@%^!hK2pu=BjwrmrklGlp5UJQO#n{qoAdRZ|Hb#+fN^546I zmyVnUikZbI96)0uk>jkD$Vt`}A@x0NU-pg4ztM4Y|vHq9|gwGvjViDx4HKwg@|A zPJ~IB6?}ZNJEH>FFx^X&bcHmVt2=L6Lm`OAVnb(C+MSK#8jSEES4K?@XG&5@P%sug z>r>=K(u*^HE#Dt^|9-D-kK1wc#botZKT4ahimLrekK)9{Nj|u|VvVLpmsxi`Z^x@1 zzK(I;p(S>m^{7)KD@{&O$>DI9|F#-eme?{-QICRL_V!v8K{w@zYF1m=lFX z>W=pI4e_1rY;^Cek~N#$uewvwo+&KoY*?ABxsv6snHm;CC}sRO$V{|}>r8@dTKMEo zDdgTxl6eJ>T4>F*nHfZoT#J;e7P-fs)+nb6Vj;*lWy!ad1v=Sj&EA8c78AqLP$fl0 zIj&d(y4QvAPy8v8;!7%KvRC%v6nO<%hN$T}sI-`aG_vQaV$C3o@Se-MSLjOi`|tR@ zhBG1MdQ>+S=<`v{P>*Fp)ktqnX~=5#*1eLeXICuSsJI>!YnO2^^BMu@;A>z^_94CU zDP3D_$0~|COgP4Xx@Y1j^Wz$nNBEXL5SQ`PXO^zC{n^{wrye4BGk`{k%yJ-j`u0v^ z9D8-3FxTgt#yuGZo@+Yh%_ z%)z<~gR9+Ji;+tT*5IYjkv{trk=puPS9R(_ViOg$*%Mo(W{wJ-xHPIxYvhNyFaIpt zi8(yO`uL7(-(jnMa|5ItJ856eC(E{-MHwjGgG}BSzt$AgBa8}?2vD~SLhf;RM+5Wq zP4eF1)ic*9$0v{S?p(MUxq!EpFqzYpoOE3n5$Cr z@ckYc%t`;oJ=@lUHQPVRW3sktZ@q-|y8XxXJZ&F4gak>Z*d-irj$g12@y=5;R5Xm7 z!XG2Vk7`Q{kuBgnFj)h~`ax?y_wz_E`_E7JIk7c5jYgM^v+YWYta;_@I0q}IMAq4d z;C)nj?*~+N5IE5UtJL1wSIm005@1aT@O-R(`Ur!*nJ{|%YEV)65e-D&sGJRUIF-8P zZmsy&Hr=*du$U<)=r_BZy)|gGjQa$<=dS0S?OTy_W7OYaxpyC*u2>c6LzvQnInOHn z8I+A&QV)#(i~gDAhZ`;Lm^_YMdSG2{d(d3x!tt^3ZLR-yqYF+|jB5fGbt=iTv0BB* zuBnYVjSI4vWVtSvlQ86KIVa-UE=~9)Le(cJV1T{c393Afuzo%_W06+}`fqM74MNax z+_#qUdC?mOA%0ng0h`C^pHxWZZ+W7S{6lmX_LA)UrXQ$3&@f-HV7s8MFDWfO3Vv&y z3o@FYBU>|!R6~lI3`zmjk~vwpfDe>$tp$P|KCljh#lt3xm%3nCsB?-YK@k_F-;&Ar zG@(x|ocwztE;8;=>D0JRya{{~{m&;M6Nd8nFsT+gy;kvq*ISy;4LSZLr!E?<~ zSGJBPgzFPcU5cq1t>5_%7w*TU6&?QaMQd1vTBT;PbqFWn$JR%mMU{SGwAUA~TKoX0 zN$^@pjO`QWlLTq9^IZ(s1Sl1ed9cO&{fTL_JS{C_kr9aevNUYUj<~8vXauUQUrUSa{ z8(dID4rOTwq~?cIwt_<8;P9o;^W6770E(jnF~9Cg_6<=f2y2Yp^bLP>rU*_ST! znR4H20LmbPQbl->{KP<%Znn`AwjoK8C~Bh7T0sDv~Wp?V-KhJXZ7&D)w+tg+8olN?obabPj_evY}I zN6}RV{}0CZe*N zM8AjntHn_1Bm{`3hQLbQ>C%Tq*;h9O1gsV;7R&vwLGUMCLA0TLvCaR<&5jW^I%T)M zmw?`^A=_BT7u;t>B+T-fz70{c6r7*{g`);bJJR`9Q3Ij&hSm;B5Spg}pArg&dOZf! zK~ZsTuCuE;@Dl2|I%uRowsOdYu!azV{giK82KpmqGj8e#Q!|LY}0+t3v>h2ge*8%f!CU1rJZP6LWZA+4f&rE3Rsv-h!~q;`LN0S zv4>gw-pQpJr-7vJpWyvm=Ep(`iGj_!SdaS#@#{lwwS!e@X$Vg@&y*h58Y-ymXi7K# zYWzpN@v*J7+w+8VchBrg1Pqf`AcTJky zlgId;>)MEC7egXkw5iRXaFz+eA%{nr%&j;3*iPj8r=qNTZnxnRpQ$wx`1W+gA*6k5 z%3)$?(+onmUj4p*pdbJ``oL9|n6@WTX)V4eJTZ-hT(wLJXHD#&eVrIU2T@J7IqPW)N6UM1cj-#lxYNLrMIAq2IAl^Z_R2A6Z~EILN2 zK2LAcEmXEVg?Oseae02cKOVyAjMcQ|o)Y5jhN}s2jR|z{SVQ=TjjTzz!CPuc;lVpg zVq-W(TE$B}40tquhM35&R=5S27saQzi%BD?Gisq{f&rl*edl3hX=U~`3(A4g8D?97 zCZg{qR8-H}jjo(QJd1~dEUzu?vxnRvQ|jteO<^+ApW3)m>=w%r^AJKD;`1Ln$UPT* zdlh9c^=b7C2_8nn4}92>BioUdZLLQ;4FKl<23#B3(|x`2MahP?bOo$mryMS>Von+~l6Bzl*q1V0U9izK15QEf)!RMYW2umx_C^!ZBH&>X4W`2N3mk6v1!G+ z){89)imt^+Xjef^a!+?}pX$J`tFJ*2$-*h%4$l;{vD-VXytbMm3Vr<)!f1m#gnTpG zbonFGsCGid1Q~G35H66RpDFjJ&hdg0(cSl&HiVbbp`1YR3V!jo_{l|U-Em(0md&?qt=_l82NCVews5xY=(>kz?_!Jb`^^{b89u&^XiG4U zL-<)NEQC7-|JqRO@xsS55ufXoxpl@NqY!HVpq!ncQHso`tN&AoEXJXLb=VrBpzfA# zht*i+*k(^f6)ht0`W&KCX_9GKi~#3X8ZgPr9#)f$vcQx(m*zh}nTF^eC_Mima$Lhl z)oHt%=#l7`Zg-Tnx|R!?e+63&h?%%q5Mlb`I_$Lx9pMO;iaLMdI^s}n>egX^2``J@ zgLIXgwYa8B(VhHM^}ebWn0hzz<>}d3+CM?ix#$&EdChy`SDc^MAkE_IWoFwa0++F} z+?ckaACB$H#5NCfn~5{9np~}Fyh6?ejfKf{c%~h0dJnOA4zZngh}<=av2(weJ5!)U z)@sz((33n|f<5#^PWpK01i$sy=!bca+wS^BSXEIzaj(D-z)tWDrEY9L0U>a|SHE5y z-QFq^|K&KJ(-pQ6K5XqJgwKk%w9y4~#P}InwyP(Q@qEr8_E9j+dSYtO^TU zcUmk&8#X9d6q(}8DlDddO=Phb2X6|(kc3wJN?nF8K>0!C5<`aEA@14xTTIZLnjn*% zVx{@k*RX;l{C&fkvdpvtFug9+4Hh>Xl-paBwhW)*xNuV><|_k1tOFA?lbi05W~3OB zF2%HEOysX?`xY$!Sv7Z+Ux&PPRS;!tdJZ&AMpju;s;a3&d{S9v4){ z5%fN}@xiacvxI^TkW>FpePP`r18Cv8q1dG-o__djBX!=4^yHt7L0{$jFMn(|`zW{K zR=!uvS75*GeZrg#8apnHUV38A73|HKFuAVvK}2e1Hbgk8T5C4Vy-Dmi8M6x*z1q&6 z$Zd3T^%qcGcY};DUm*0`vQ9_{2cU!B@OV|T4$F+8=Gsbs2BrTR#Vx^9RMMwi@L(Ul z!~KpcqsWVd+1^d%U%(>S!V#m3l&{vK16!=QjoYqm-)+S$SEfBVa$tGn z$oCEZyv4C7?~@cCD9sae{pc3a>uYANhZ6z7I`ZHv#M;Exwa=2AiLL+hVX@uBRAw-A z>)nHq^w$C*yV!VN!o^vhNd<&uI`V^nN$qnVPYP~)6MQRjW&OpA(kfr-s35=1|l_h3KYNXLRDckkzaKZ!z&K5VRF5Fl zHgd493)6mMxWeYM>owvR@l<_|p=WZiOH(^yn?sDf$qP$?PytGK@d?$MbRkJivU77=^@kM;T#p$g{`_*msj;493c)Qyx(LWX zjo|9}a4CyeQ>h2!Y0}%y(KLb_y=iM`Vpa2@lI1o3;XQ^^M-nxMFF zhYh@H_Uqx?(VC{3Jxu=O9lU1v!D?NOsmzSdA=_ZbK+7#dILQ|itxyjTV_q*SzB^A?S^y<_HiNO{9=J23FkJW9=G6=xW)JTkC&Lp122a+O|H+r!855$L(=#s!~1 z0dexl;y8U+5QR{c;T~yhVfLdVE0G6II+pQgwxIq_uFCRjgVan!!ObYYCWW#M?Yia? zD06w99?(_w$%~@v3NgL!GoT`~q0x^O_AuQ`59*SvsFtAZTJtrp>JmX0#KbxktEK{1 zpOitl9Z&bcM~{56^;xM4QE1(PD&NATh4GHC(z(f3k{hzljO%wT6N)Ei#&=A&Ju>($ zu*ym1Zd;}{*v@>I2w^3NJcM2df7N%(*YyRPyF%<7JmLS)fM%WO2f;JYO6F1|!>G*9 z>>s-L%<-1EE;*BGI1EsVux!x(*wAETT!#u+W3!%6Xd5B(nj7wR!66b2jNtpZ>pO+_ ztp!)B0n#X#-ZLbKN3&?19^sg!XAvUnEBQv6y zV1>@J13D2^?{yw`+jWz|7=F*)*8|E@xIaOnrkF-z%+7OEQ+w?W9j^WAIT)2)d^pC{ z)?3y{h{S(7k<4L#S9<4cJQ^KH(B)1JOgB((O{>ENWJl*~EywRpZo`G2a)@4H%|Y>zX~0`CzcMJHXt> zB+z`XPQ_}He4Ty$_TC%F)%h!`l*HPslY{v@jBcN&a)?MPK=uT6nxkIbkM?;D<478R zQd?IuTSs}e^>^7B-Xc)-Di zv9Bbi*Io2fg+1@tlhH$XkbDfK_V^W)pagcH&tr?ZEq;&l7mp$;(Gd?D-nz^X6Y({% zeoUR_-IKk5NwCPs3yJ8WKF9o+?WKfY3FU2TOj6|!M==L7x|?*JqBIG;UV(%mbD@o5 zj9Rf;5d&SGsTZ`}$AV+<=H@AiPs~aLm$|57pPLxgS9|r_U5pZ}f8xM=vN^(cas1miFb+7~o)Y#czthzZHn$Q= zb00(%cvJ9tWo_Z~a$OccW0goFMbX;$$Gfoxp%Hv(&2IrcKFbIYui|Y#zMP6}3@4#u zbv8{od$%*HUMOL>UhcB7$Sl_nHO!^4aLdZ}rx+7K&X^$nVbA4jP)2?+{()ffv*q4~ zUQ=CWHPnpi9vfZoNH3H$-e62@^6V7XQkKNyZrOHS$~4^=m)ziAENx{@@OP-~bF;Pi z!`DRR)wPab@E!A$qCjb92Jw{Lb67bd?2QZaFtJcy#H~=t#Q`z-KI7z?v9obBnCm4I zfX=m)LP>Cq&2?Be?h4b6P95xe#8^RH{UfPM-yyDa5S0fwJGkd#In;^noHJJ`31QXz zZm_Y@=yO>hkcacVV5SM9T7zjjEezP0xUDRl)L7a~AUOZr(|*6~$h zD)uLp5X_q@CqUB9-IdFt>!rJp^P7{gEcRgl3|7)5t{faAB>wusf;1@Lxo_^}!(i|3 z&4Y~JbM}7UBo5kfGsEm>9EO|5S|3g^Zy&Fn`mc4|Kc#bw{amlP#|nV^j!=Bw2-9pv z%W~})qcj^N+L)S>un~{?__O-K=X29@f98%EB3NcC21*AuyQC)%-yo>!QzY0Pi#c3( zn_j7~VY?7uDK!!y)V6USr5YZCpSb zW>y_vNJ1_|Y_GmJGWA&{V2mKE7T=9OV7w>(1!grMqM-W>TB{+@CrJWRSH4f~W=uL~ zqL=YML~^x2uD91{90zBk*lxB!Jg_;jDrkP(Auf$Eim*T(AoFX6TM>e*(w$PJoQ-WI z-W+8PF*_@*Gk#~oTOIZvjb$=9kRfXm_b8H$8Z7#I-*2Pps_GToJGZ{SE{8zg!()fC z7BA$Ui8z~h^NeW&tHk}DIBynxueoE{r)|m?Q|-&c9Nab^oz>Yi+h4H(Dy z8PoX3QmYv-`(OF4YJACNv#nSBOy!U2j?BX^cI`@>T$|e`qHmvZ8rbr>JGMx7gw;71 zvN7J@IN^fiJHf2~IG*8*wz~vHf31`KTtfy5`@x!#OT|kgim!8x zo=~q>^;LYYw|dbyuDSE|JcnDyD7`OjH&tu;#Ky^+?bX(TU0~nMzh2{Tij`N?b8c+6 zEw&_TZtbF7k?_l9n?E<<)O`jCNM0~d)i)acuyfE`3{*@-dR|vXUL%(7yctzR)EUwu zmfsUwOeNHp7=(qNBdlf%3x;uN6e;B9!n{q0gMc23Z_m*n}>Gn!y{S&u_UGW=&AuzrfdlD z1A3IKd0|CDug4#zdM|ye@fSC7(}c^<0t8(WK*I&68OV4&y_RLweJ1F$<{l@5G?*XD z%}O4%1{k*k;Y8jZ8X~dBt(Yo|eBCHCbonpLXm`~yr$gWT770H#A3s~kI$YrVNz8xW z*vQu)4A+)qHOjouQ9H2-a#nk8WHoQ^ij5Y2cBiQ-@(KIHt<)XTdFG5IV+tmTPWDHB zY+Pr$W(D2CxhW=Xm@3EHK9Olv^_#jsfUYe|LGho%I#*pm;JNR^nepIXTz_Hr+-*IGi&x3W#H$@M$^EY`;AZ5!&H4n zb2A59nuRDHyi=J~M zA7nRNzdd2c+R5mQzza2+{HLU9kupNFP$AWoDS0FgGt)ejaWiAQjc+DdnxHzH@AZ#I z1anSuTGg91xC)zBo`t%-F&SnOaAkas6}@GTrGho&-=q*UBI0TZ;WX`;z7SNBOH~mK zebC9n`$!>mAin5L(H#~gFI+w))x5!+bd|Q90B?$9Z~T!)t?K-nrS#smIby*@o|&cqAJc3s}Vg*mH4`md8mkrm}p#8c9iuK zLYQclY-1*1hNF!qg}(TrYjj#`!0CW8%IEu;J|S()&W9bNEC1|VsGiz4rYW)|#mv&u zs|vkF<5u-=%jdsM+jO)i2K*DXE&j~! zN~mOEsV?Bnb?ujK(MIJriR)ZNqjDU0w@$vf&|ZsOf47Gic(CoPs39V700RyojOZyp zp;1ymRIM!!uyFan02-pKUSEZ>Q$V?hdn1@9JsnqiE>ApH^HI)lcd&`vE{u});mUBg zQPgAzc?aU55}h2P)EYS!V=K^@Ke^ukGVJu6lZd|v%eVsCGl@dcjL zsY!w?j{^1j-k)UcL~uuhdseTm;TA!7H+`Px2kA(Rxuwmf$gR1dpDwhVF(aRZ1MJ)b+ub3!QIG(T#uLbKIvb0tAd{5iacr!M8=cOLDwjp-5$ zW<1Hgzr#lCn#6Zz<7%C31@Ar`9hHO#GWpK0n3%8WDoY5|q%KcMXzD<{DBK=ehmRRq zzdQHsXyAjRfw=sYBplx%dX^?4@VDNF+IDEk-)eI~ibuC_OtRQ)zx`eEo12)n&vc~` zTX8|?flvGn&22v=++L}>wlQxzD9hZvt|lFHyjy{uiqgWnS&(73_Q`zh1gK^YZd z5Z+2~y-Esi>zijI3_?T8da`T@_7DcMltkTFPw)KfpAGyS$&pR$A29bdqtu&750W9qA%!`B+`owB17AzEs z2EJDbSFCHJoo5|1j;5<+XL)VN2_+uW6E%ravKW{#z`z7~czH`vqC99Y&B`~?wh!igQflg$KxHEzf@ud))+J3In%H`%@P^T0cxR`^c*b&ApYlgQ zMa3r=ZzSZ*$?Om|Gh7A*4SiY!N?z`cV>F?2bR2d$ejjEW%n-|%I?J(FdXCp}vRt9! zxX}eE2!5HdIGYJi@j#tJY@IVv>|@2e9qwAp-Gg$ zzf;n9comyk$GdA%eWPcxU_KZzzT8#4>t!N0ZqswEvomfycrK*S&$!;`LW^_2#JGwM zjgEw_k<&E${BP`pJ5LH88L+?7(Dt`;^tgLC2F;$~>X)gXTJ#|5I96cf`Q*S08 zw9PfhN+;}oGP;~7ZN%TuNge|61sNk2cRZh!^*cS&*>3Y`#<)9%8eZFw6MG*n9&q=F zPE=NM>^)-?nHG=kfdCp~Seo;TJ&NoYp{qdinQrVDvC!%hBF zL{k@p%(vK8w@6q?3+DRT+=9Gsbz*ep`GONyz%A3t=(h2of>km|la#{cqIJ7vPuHMq;C z!z11N@LsK1M`BR3e|o;oJvxiT))^P&XFS(uJv^^75{pHyDRBCDmtWKEJsgzP{3i0g zbI$NaDc6G@N^$9kT_TymM#l9K7|`h#bOR9imz3$*_csg>YWnHtA5R`{yw`~EjixSq zzt`l1JO7YVUHD`99mF%yp6RE7&Ae+K?cO(|2q!|CsPrIb1@*QhwKJuT%u)T!)B&p# z4e}DzYNejz)lR!~lz~?(-2bteTHu_vXPt=OzJrLZm+R_H^K;MJV5izNQT}mxtrZ%o zutVvh+N4Lj51WooDt}YyRU7V|6uIEHs5~8^t5H_XyC+qjW6C#0H=e^_a!!b?CWj%7 zkD_EY>&+Ft$G)04Cg-B>gqe4`%X)l7+IHDb{<&i|JmU4w)~foa@Y&)7U-~d7@&cxaU8T&&UsmwN|D1IuAe_!Mkr@yEGhAd5~zxlvO4?c$b*% zMvxVk)9F+!t1uY@=g|_A)nmQ5`X1}e{1;QbrxVihwaEkZ&8c}^tA&+PvM__>7I|sx zg{i$_*r&u^yv8_mwSrzyJ0lAdrF-N~gCrKvc99vTJbkCVWRnJy|IbL1wd3QyJPGUM z*_)hS_~9i1@T4|-zgwfYGCe3?X4w!iPNEjX=U)XCHa9*Rp^>f6?roQrekZf7_IJnf zb&P9NkDR6=TC!H;>gRe#BTR$UDx|_llhX^)6X9U&QS5# z9h6xRAICeE|9d9pR|N}j8kM>zOpw?|U{I*I+LTG{dqq_7@Acmbq_${29$FVoa?OPQ z+&qG5G!FZr)q>L-*IWszr&fidkk(3H!L6EbT7Us8Fbst#s2;-)NALNGy|C^tak^;9 z0|h@~54l2l1Yc`|-3hxW!ZFBiv83jjv!jfrk;I1;?8*4rgfoqX0>LGve<}Zcl#%NS z75{YCGdZh}4+=SvoqjQlX>A+(sO7C^PDd_|8HT&w|IUb!sW+Y!7(n@au)%)JcjcCi zL2Gcl-Z0H$EVI@4z|E_^WK!#8i2jE+N0=4jeEE2W8BZmpCQP0*V-22e>E%HtHTD)s z2TXxv;mjs=|3&38<+nod0dSYE>?GKIV=I!Y;O1%JuzjG^>xP$(VwG?S%Jl$3eQIS0M z3=#Qt?Ejzpi1+^b%>BPijol&~7YDEPe@32Re|3Jt@b5TH_-%>d4vbs+_h;bFi@%gQ z{X@Rr*?(t%!tZ+c=l>l83cuA5k^Vag6n^uiW%^6d)9)cfSQaX9SNlS}5)44D-E^Wy&^jTJ9>(Dus{eZqj-8#L>xZ)z zW?5gNagJO4(9w^ac$NNZpJL3UJ@-Z0 z$6*0)+#{>Z?dsk78uv=&5y_^%hqd&<=6~okAb!W-c4ii)2)!%jL|>Cc17}T-+%Xgm zjPmjPxG?))j8o%$v^mon)hkw04UZj!#?L(X`fBQBu`W}N7q#v6Nl}$$KeX$+kK5K- zvh}Uf#ZAaHK9yys12B_4i`74DGBJoaI;5Xg zvG0r`DD3qfwCmfH?NgZ@Pd`z4BwemA5&chh1}YY1Wl1^mhga^SaCbOZ7^CLZmaq|vt8;!hd3xBBU+yaj|7X*uKA%7 zmXbfW_s;$MP-URHJ}tPJt?6qT6)gA771@3ymeYB0sI6OXsRqL;c zRXK<}z8SRS_PX}bh-esELJi3E>E$8N6E=>?`61Q9Y<>#nu(w*gF5lL;CG>gsFLgX9 zwJJdFQZhkVx25Q+VG`fVIKNGK2K)-VFSE=)Y|&hu@$DFOf7|On18EV6oB#$MYAjEH zyvm&((q$xoUva53S8JCM!$FWA?^ddR3I{jm%iNf7uu_O0kf1`-ze+`yz zIg-i5(4FSVAy&dp&ng7X0%z6zIHOjlzvp}baVsOny4*~pcu$m(&Z>MHkLlu`+@%$V zQl*yM|1mf<%Rn0Bqmj0h#5?6i5&W?8Fti-@HveJTFg46|gFN=ahLVe4)EqmnPmY#Y z+DaChcjzvz$p)hCQLUfiVk~`Zl-@)aFW8x_-C}GEe35WUmBb=bpm!RU-U`y9D4@5Z zwN4mQPu$6U;%f3Q6Hb~&0sp1`9|MVpHibUBV$rb7gn;YO4Vh&Td4Ve>0cv^?uWtT5 z7&vjw!Z#;3-Gyj9sNcSt`yZNMfF>M36Fv@Srzos2-n9Ck?bCvCbAFfd$G7$qJy?tT zBlTeqog7GzwyHZncqj@gbR2IXh_Ks`+?h#j(UG1;+HG~(9U#M<|Fjvik3E%PSEh}Z zsZTkoY%mZPJK6SueRrW=)c?{9lL~r~kbR&0g4O@KNP{-b&RQ_xViZqXN%f;kAMnI3 z2kU+QHoi#jb^Dcl{AklHPft4JT+rSoyE*uWH63J3vr4cd&&TA03Ro_UldkW%Tx{FY zJB!)-z+T8C(*Ey}ozg`6b3~EpkG!q}0Z~wj>`r7|{Cj5hmyd=X)^BUoug|r--_TI{ zyw`uS{Q`U{8!mcOhk;9-Wxv?A6;955@z`l(fzPrb!77_7*p#AdVzp41$b}+hn*9k6 zp-654Mu&aBSn}xqQV`w7(!pdXYdTe5e$8ed?Nj^i*X!ljMsFNm35L-zkb&o^Bmaa6 zc`L(uTM$gjcaCRR8#^2LoVrym!Vmp*pR(2?uxg(Y$#TsIe1e?k3_OBAK}?LLm)^?= z3_DM3J2=xFU7%pbTzCrLYLiXN=`V|9*)$^8fJq1`+op%;Y^VnOtG_dPfKxYr0c_iX zA^WOkr-55edK66Gy!7_(nyaLgE1!Zi@Pd`eH-58Qv%wkY5|BDv$LgK>Theg`PnLut zp9hO`6dnTE%}>b=FaXH@+e_2v04te)&5%x!(X|uQfdk^lYENc z6DKvD4Lo6%za%X!EglP=uKrg6qQONC|DeuO7~&Ylz{9p^XQtA%Vd8xwh+Tm-Eeo>& z7Bpp>BWB3cr6pb1R_b2_L(4l~?V$0Vfd|*gDQz}05IDmz=5#Om_`s|}jG+K0hm_=( zZZYckw=}?h2uO=k=mN-ff#;*a;^Dpo!>Ytr48pCsX=$->3VuNq&ocQJ)O;c9G507- z*6VqzZJWe)HgFKcAJm%sk=o*sf%pg(+hM}PnD4$rHi2xgI+3NV0l=9r z5%@gNM6zakUaCF=Ktk8S%WIQ<^Xll`Ta0w+1W0wK2z3A}`l3lE;m=feRj3z6vI?6` zkUXI?(z3^E$(2Nid`tpZZ%@8G`{k+6@$azED2J6E6$<`dq<+&xSNpb{aKh3jCcheMEDjFV~*%o)zrB_B*7T&^B-UJmu>(IG}K7`SmAtvtzh{ZVzPT}=?H zSG8f!ZsM_VT4EAS_RGV8u2$WZXjePy_Su|xZwT95@@3#TNo`7QXhna8+wFmp{n2%StlelqyR^IUff zj~|jB;mraQG~zS8kAO|hA1eYx?Vz9xJG+AwMAb%mc4dbe}B&X z<8JS)Ei8} z#iM;RQYO-<-;H|xB!9B{IE}E>R$J331G&a6vrJS@flM4;#6$e*1-R^F{bVhW;xSNq z^NxtuZbiKK;mv&~QfANAh+Wf-9sLCpaOzjdn9@6w8%)ppB>)ia96SI51g6)nG}&7? zB$8LqmEd%Kvc&x)B@~-ud+h{2lX;?K;`pE-j{zfC?RK=aPbO2r#Gq|uS0*= zEycR#LRi&qJy^?};s<^iIE!Pjo2$ND)~0~f zVS4K4NmrwRj$={ab^rvCh)gUJKbJ=j;o#VmX8=S_^~5hcL12`uDg|H^|J&_*GJqGU zcO85_j-36&4h(WoTv>&vVHOLVS&P+O|1s@`Wdg*!1df5mpxSeg1bL}9t^t6NurTx# z1OrwD5Iol1_i$ja{5vY0vdD7h$PKAaXh&aw)p2(Mdr%#fv-&O62DdzS@lP6}Z47ha3~Y zBlGHi_O0Q-@R4iR{<30#wWLG#P-Z)Lu1>?ng5zO@&2MRTw1FScyAG|!^wRYZKUj-g zloiqUq(M>)=Z@IX4`r?)@=cEs`LthWn71IUNIFoHt?2_Me6^y+Zz zL+s0^V11-3lvK*=bUycAN61g8u}97S7j;nq|RIcm(>hx1i)YV zjjJz~!Ws|*XPqwvy_Bk(SOl&u!1Z5Bue@RbiB#I)MuTi9jbUZu?(s>a6{!s%cBI`Y z1ASVU6$Q6@=`*;gcalH=^<03e2{-EBw3Q3ggUmH`I(u%M5UvM@62QpZo@hX51+pB@xk+Hp^Fh=%te%q`zH^yr0e&J!(^o6@;3HF=uwRTXwa3GCreWzgUJVC^e7_0 zRYFeJLEX!}sK@QXdiQVCiT(QX{)%rrXb6S=sBng^t4NhT6kXkU#|wL7ZGB9?bRiNk zW-_sDDV7Ik%SdNk zMS9#wZzxGD+L-Uh_57Zh&vkur2Mf0iHH|z@;WjqV2O)z`0s48Dm@fT4biD~U)NA-Z zJW?o$p-^^-7Hfp;YfM6=PPQQw31i9Fx3UvLsmM~qA#3)rW-l_fjD6oHYxecMN9Xrn z-uHUXb#=NT%`?yUem?hSxxe4pF@S!0NCM6wwMi}gKhD3Y4^&&#n>UA?xY2Jar|D1y z7Yyk42h}PHP3s@y^&W09pbVtA#_8f<1C36FW9`kUHrrz*=(5y&5LAbG(*KH)kQD_1mQm!bBz@P8>JL^Nw@qM zQXV8XcrQ6~=Eyx-q?$^3I=CUKfDI6YNs_?6q9Oqv6kQILrnhqy9{%kUPR~8`??&E0 z+mgD$6jX{J&X8_}l%gJ>jyiJndGbPMz^cno0w|O7>K5)17HpZ{vq0UgyM?&N@ zNqp%q)nHli2A;?$lQn1lVeAH{PR$X?8FwKy9>PUNyX$!p5v|5|bAHHGeL;=DfCJ^gbi zp*D8!1OGo({?{zO^whQ;&QFC+-~%{gaAHi}y|W*z2--CKrTNv0GPl3))y(fc+xWpL zyruEwdGwBpV{z+@4*=&S4o!-)pCG`SfhKf4*rc3Xeex>zI&8A*+>Py&_%<0p5D7Ng zJ+>c3GC$R#>3AuC6YfB$ota7_ur8vqD`^t6$-jMu+bd33#e;6|1k(6MUF~Vb>I}?^ zIJ1=Bu{wGBfFR?B1%4mdpvNxQ^7$I@PEAW^W^@a@-3eM>C}%#y!6y)dnE!w9Z6H#9@qJ!<9k-$#&(eQtl_P<<}Kx7 zTl#`^;mum#cE8#JvBqjsqZ8Y=+yH^6|ojdJJ(RdrSQe`61Ge0 zH0}iB!5dr-+Zrdg+%$L>q&9^hfBOibqP_P^tMNi4)Z$QfuVnC&6POI}J)nXEBBXN( z^5hjskfeg0I-5ZVrq>X#y0{0P8N8W`_Eg?7E+D-Zy$6Q#xb*}0;+9r zk3DmbKRR;7CUqEf}xR8|2aHdy&SOnIE%8(_}U zq|Lyg9DW;=y&v_G{JL+I*hjp8I4}`OOc0prv}d`ze}P*rb-_}N)CH0cL#~sr9{zqB z&h#gJ>2afBoR+ocl(`pPjUl>_48aNKiX`~|<7pH*iKzgsm(ptFBD^&HEB?1gGi8pZ z^wMBnkq`6o-_>!Ae;Vtqq5>fisH)tO%|hpSEQ8OP0--PKw=NIG!ZCM@F4Wv?K0HeA|F!0+L<9a>zzM>VfKT{CV&h^N;{)Os$STMlECGJJ@uePq^JU zkhAcHbNwjL>Ar+evT<~_JV^;}*d7KMz}#sz!Ls~wxO=pr*&vm- z`QUwa{?G0;i{Z`TZYr{(iN9f`i3N$o{NH}0G|BMMJ2Zskpl!rGm9n}o0XPMfd#&tm zTxY|yVA&-9X7~p;aX@e*0~1~|Hbxjqi{-`V9@pJ(%R4Sn+a7?#*T~lJmIHR}q{k(3 z^j)%YC&+DAZY|C|uKm^c{_TleBl2>LVO^os4V4do$XPjwZ8UH-8Ta%hK$j_P(t(9! z6tmxB8@g5S%&N~LajlW1Y;kC*s?KP}bFCnA1lI?rP$@xpwN z(TX7*a;SYlOmR#FaPb!7zu;Yy@<^z1TAf=q;X$Au8H&hw$#yM!ryXtGkZjObJ*>!Yd~{Q8r?Fqx z$i0>EkEO+8EtkJjK3;KW6qo0SY_@8!Yfx+G zBTQcHyH=B{Rq_wHd+V|x-Le$7)?JBqlTa>W`J=%MGoT`JKuQiUKfwNx;oiSR3M~99 z-v8erhWk&y-7=wG>}cw4yjLfw_WM|CmUQb5xIl<7TpS7_guff4nWuj4tIJ;T+kFNj zXm<9r%r(8Rk0h^2*GM{n2lIJRu3<&>NBZ|0dwlAZ17_3*!9X;$aJg zZ2o1?unssV5sxD;^u7mCOqyr--}&dED;wy{b+ADEr2=+ZYX+{AW3T^pE($ObtjX`$ zdI;tA%2&M%7mdE8Z9So{sjKbKa*%xKmCd#KtwPrelL&2oZ5U2osPEesFIF{ot^%!} z>AZB`bNOP76{oiscF=_d$!8>f#MmnBc7p#};e9j&dM_-Ob_}u!Y8L4%BP60`vg0wk zztjlB1|-gqU9b_2uk{<%+{wY#yv_5NTHMVQ_9?p>=@JU{cY4)u5Y`&SdIh2+MU`^Y ze^JtFu7ha0r@E?8c|;hy;O7_i66igR4X7=MT(nfFhKj(^!o$Fy z#C|VspSf{UJihQhbDYE-pm+*quqzvTn%}dxPWo~#(;in|Nb(S~@A&2>V*sQ-J#M6` zaI`9Uh9D|*ur3wzZH(tRlM}v>DFmOKX!WwJHMAidIky$ZxlHO@s2)})hVO|7$2h!h zU9@IiX-~1`t4>|%m(q^4!+*TS4%Z2=yP<5O>2jyNxNMj(^*(rIay>V9p$+ek#;$!aU z(pqIyVSw_$vs|m)mQ7pmk~x}na%84y3H`cqix7=`6<84J5%E1me*iK&AQL(rsfzWg z)8|qn)QIm+l;IwA>OoTa!>f=Or-xXuE~eG%Y;$nEfwxm~ zm7%oLj~wLk==QbR8&WfA>wPvq9Z7j&vBtL$E@%$2amEzmk|!lLBjqZbkGUnW@te&p zEx6Dlwfm;`LW3j=Tp4Dl8GxW?3KVmf+ z7qusoRCSAIpNk>5G*(L{)NAmM)A#;*kEag%jPIpdyj?h&-%>!OwaKO;muYOxTz@XT zELBV;-~XvHCcT7pG{eJLLe243n!o6~xv$%jh8kMT^{SkDu+onU|92z*1=@^zIm%hV zqgGXg^`!l?7Fs2wwaj z$WR1hDvv8FRL&EEHsMPoLsN!B^TU(8aZ;efp=mx}IF2rHTC&w9(X*eDHWClW*gA&D zy^ep&y(((*&b;8b&e7tLz5*BphatmMy7%XL8RL%w%9H?_u-Tg3V!E31StqEhQiCax3yp zMCKh9e(xAuM1p?voeyOTUuHk%Ce$b`Gu}_aothwgf zU&hqzo+)(52G1fC$FBYR*SrYlI@(Qq3-HT71ILQJIazGW78ocS$@jc(Tv)Yx`E?s!5frH&4BX=i>aDVF68ku{W4HAFh8;qIB)D9F`pYHiCC2 z-5r6O?lT{KAiw)|)I7nhmGUkFPi3;xiHT|9Fe=@|znNG!lR=Kn%Ut1oGjtr|-enN! z|I2-G2Zve@I!{2Vtdrim%_%R5>rb)p-a+Mt>dF!O4b@I~yVoJeYtHx$Pbq>;lZG6)Jj4o$7T zH6WZP|qjnb~R}jw24-TXGVwUqjQ3pjyKz50@xrnO^pSP{? zQVn7&@a4C;F)UvzIhxz9X^^-MOAqBk&A#tyIXWBdqN%EdB)=Rmi3_2AAajQ9mWIze zcKZsSGlj!^*~Bl>Bs79v0!W;x%;M*j2!|2J6h1`pPysGSTQ!*!JddAXGOX4jOzf)90m%xq51 zZ9)-T%v22bXK{t{pvja{7`1yYO-DCymCV$wW$j4id4Q{# z(F!NL^Scsx0)!(zi8Z3KwY;~>$-;3KNk-RCy+Ox77Z~-#Ymx}m7T7)7gH61J_&0EE zMTDyw9d0ha1%C~BnIg%m?^2ysT2jt}YF(O#7%GYk5rBHNNx5K`B2&Eq);d@%kfT8% znU6A0-#=-iznVz$S2981{Q<`bY+LsZifuAoUl6JENL#3#^U`@G7ZFY2SM42XpoWH< zRVWtbB)gt{6LeCQinGd*F=L>0#l+O%xTiBQ!9(hV&jBu)GM0mOuD#;a&t^;Z{d)Lr4Qxt~Qwka9`N z!`fc%V0}=!InFOs9?kZV)$v z8rmLU7=UDkOWO|ypoPihWpSCH$%?tR>t^j*{7%Cssv8RNd-=Lrt$`mm3-@poB@BvV zi!i&WuL~Fk(_4qrFwdBc{IX4%QdJLPpVDJkTD{)Ew5qd?(|apRXSJW= z`O5*W)P%~5uTss=e;fTrR5M>LB@3dMJ3N`}712Y$TP33<6wP){IDR<%TZYZj#$OKQihvAKn zy|jR4ag}sHA0u?{!#Qqn@I8C7w<0JP#awbtaV*DrQ{DBU#2(BQ-T7~>DD-rpyE#*u zsdY4=J9v}vO+RssJn^JwOu~NHFdzqPrWhFJfMg2>~sn{Dw57yJH zj(Psl6569*pn0$c$;>{Oi-4;g7v`tOZRX_Qq04!!6l=Wu5|4I2$JaT-AyEUU{!oNQ z+=WY#EbJIM$aU-FK;Pf#cV)f;owF7uZpd7tDll>;CN;`LYMR?Pq_vn=zMYQd-1z|D=mJXLorM=-thb|LTqRkg90exQ zE|H9RW?qE+O7m^L*Eot@8#;N~KC=vb%})HGOqBL~yZA(ce>?VEb~DU>P8N>KaJRyS z^FLNT8SQ?`=_j0{OITWhD^orr)AC~CTMMEfwCo*H3bwbd+%UQzc;2);%zfWUl<#-Q z29yxY@W-uqg9H1*>6}@J<*RBt^bOK6&k}0rW41qE1tA_Op%>VOgnoD_1C&~b0%SqtRyMf)qBSA&)|V}1kJbZO>_Y?z*1*p(i6AHh4ky&?FCMoX3D6MYTk z3??>TNS}RK4t34%63o^6jX7Vg}z6IT34PYvpT+j;iNSq zS~KUxQD9#QWZhO)ohV#2p0cFyn~jVx+mdYsj;XwL_O!%0!BoyO0T*0pzgSIUcq|-m zs@!>QmT!agcWm}8p7m4a7aEbvUn8uP=4JctlvHMq0m8`+JR)O&5i;Kd_3}=|u~Tf9 zXiqlo+|yAxCi1{sf7N>j;^wIMUjFvRKUZT+#`MPIbVWm9b?PngP#yfZQu{P3 zw?BKl{2kOB=f?~-dfnUS-5fC7+SKgHf1j-#j^5)B%v2gA8gqU-zl4^VnXiB2Qp># zOY|gG0m}rr2(8Hu^jw7`gc|uaj2LBq%Di%?O}#ul-}k11U4M#)jt2L;`|Ih7>i+_p z$#$C^^MHycvU+FJd8B8ki=acb3$T>C+b%1Gpe&lI}7B+a?*P5*P)pIsHB7LRo9B z*NZxNu^vu_ezSHLY&z5CL1=Z;KLE;>{1I1w|8AZYFSn-?_qXi>sTw;`YN=CRVqEsc zs-eAzyjJ23j5t8$?(W&v)Mg*-9Ryj`4!a?FTGbeQO)%!k42@22EN)57xs7aQF3Km) zif?EmMV6Ls4;K=H5}~0YIm}AaGdJQCY?Jr&C81 zz?Ykz55y*{oofkg_YiyUPZL74V{Inh$T3zZiVijkm~O$&V&Cb~P1(km)0Uf__#H+P zqA;e+{B`~8q|AVKhY-j3aN5t+?Syd00eRIrpMKE!9I$L9^?5Yu5cRy#BpEIIC>Gx-!H90%N}RgQO9d$xDFQ zbkZ!x?jZwuLf(zYobsHba5I(1l7pG%c^sM32+S+*owoN=`SrluhvkQ1(&`hD#Hsmv z^Wfk}VU5X`tyrq(uW^k@gX(Pl{Ds)iPG_VfIMw-Q{hSN-=Tf<=L}H%;TP7%j-*Dcz z{em8V@Krm*k&1aV17-rmyHk0)@DvW2kf32R*QaCCFd(_|s}V*KSqtN`5pEzB5*jgq zL7+e00w_xOO>bZ=3A8Z38iRWq?(!Z-melXWtqx?<%Z2QCCIQAF52w-X%0x^Vje@dC zz&!` z20Z~Gw0%|qmsBO31r6(@;4q{3wcj|>82=P~9$CD&dhwEab7l z?&Kv9-aPm7>iuQ06^9d#cOhttoaUjZB>s0VmCqRv?abkS4C=uLyKurS!tAW>Ifu5L zzY=tQnsA)_fTZrr#pd1veuYe?&{od(j4=^8U*o=p)_X{44f<8(80;`31C@B>yDMkb zZ(*Osm)ywwMDvkm^3gj@_|6Wmu9ty(!G_++oh`3;H~4l3MvA$Qcd6e6DinWLd}R7h zSOwr5C3u^|1HK9>{EI>mRHwVQm^d7qMj{uX!vkyGRD&8LDGW~@J0w!;bIAol{-zBf z@V1AxTrAZcH?^`(3i(Lphg5LK)d}jkFTw(Df3kT;!;8_-&-EmorVQj@Ug#KowM5D( z)zv27E}8;0x)aFZmd2oxVfM!YD&;Q#-#fHeuU%g-XD?`Man>)pqv12%C^(*I7047m z3?~cUk5c75oihbG%;os^FcPAYrb1(BO0L`rp*A2p1mFAT!lDd)1eVdk`TI*Ufj~x#Z^NLB_qx#d7DMXOHQv z9pyc@m45BTT~A`Xykh<7>Cj3|$?572mcidfGR|=UWDj*xyN@xKTwJlP6W!}9r6E&h zFK&OsT%AQlPp;5>!SXs2`U=7RwAxV5SWULU>Z=z*LKpTNM`v%;M?R>FY|Imt*qat- zg*l+>3zDndfu{`~T~laz@r@z#=+Wb+a~8vh1%#W~y4t!ZWRA zeKx%4zA^_Fme5?5?>eB_!uE3UcX%z7{EkdvBkKDwy7JBL+{|?um1-xi&Mz(Xk?6*X zWj;&$R?t&~B?}lTz3z(Gq=tK1pXBifFr8b^J4ciXI&W)gKl?=7Oe#%BK>&bUHWgOM&PYEn7-!mTEbH|{mx*tsR4%jHcFtssZys%B<*DAfq1{B~HSti%oum3X(pnnmiA&+WS5|A{94o)M3<$$8O*qkT4`u>x%N3rFgdPd2VL{9u08 zM0RF`E9b&qF+G1mI~THAYJj5Qjs4E3jmUKDfULQJ@|b zQx@f=OK2z4K;?`z8Hjnp8i!{+qhc%=nZnO6>K>KLb{Cj^5EXNts00IiVH|$vQNx+U zC#AmJ_@Y4(_b2L+78ZgtB3&3BVypqvh|)2~qVmCXqG{`%nR1AX^y>mMv|6aAwaJEq zSj({=-oe)ku;_RNldDw$KMjtbuDXA%ie|*imS>Oi-pP?u48$Yn#Jh5d4W%?aOx;-B zk+L(@Sh%S==|WV&hnw4YT=*%;i1YY}^Eu1M?%-7AXEdW?{8jk|Bv)E_D(It^2z$3W z9IVkWG+8Ws{b!Q0Q;1BQK9k6!Y=)8Nr0r^;RU)cpzF%JJpZm8dKM#vk9${oT&M-XR zhC}o$`Pz+9j((POHASnbCR8NV4%n51SyfV>;m{+HJJvn@ce6x*F9nf1N;#@q+n4Ed zZ=$yb7nBgE$Mn!|Q3v781-Eq1L`6%k+;VDY7l0A5Bz8fM%?4a}jQ&lx$r?-HshR;+ zMbBbywDWRF*B1P}KK4#Lgthp^>rb&(bh z{Jevo2sesn)_Y0yN7|PMhJ+;VEaH>&&nfWoVG*D0BnWJ;UX2W9N`*Ucl+L~uj-_GZ zHShd3zyI0n;lY;T43u8#Q*yaKS7{Ayx*1ILijT_~+M`3%1n=!Ce!x*=5?Rn8&lS(X zX-%R&cM3iMz`N1I`N&^PiIiLkzfYRVTsLgP*`xGZc|7w|3_xsgT9YY-)>ZL$ozFBs{hiC2dyR*zE62^!DI_xSqwGl1$il(jgwnz3xTVde7!} zs>ZI{dWg)QP#Nib!K3r{CwlYT6X`BfbUXk0y!lc=M3{<~7 z3|?kol!HphXC&QnQYENz+yC$~dg)hV{ibxcXaQE@he zRT<;up-4vznxW0X_TI4&YI!QP?s`PeJFfT_aC+fMD69wh@S9yUHDVj34?7_)*!bga zN0F-9FCGC;l*PG)#eI?WlfFZuc@MFJcUWDaOyKa?#9^Zy zMM+C;R#k~HgL!*e%QIRnrGu5sl&dfLnZ*gYH7xjV0U})uHrD+yJjIG;!+T4vM=@@f zX45$QrpMm1%$(U^lT6V`mi+$ddfKN;$97&O#~XIDRG9`%c`m(6fvyDF<|n9xdNq=Z(7L&m6Mg;dyi zTWkAK9)ZTFN&~|(v&=G8A(gZ+qFi01GYJg}=o+8oShQDPqdFK=LFAV4MtSKmwO`HI z;~WrJu{JZi3)gV$`Uox+B~4441-D28cpb$I8XY~ap;cNI3tz;UO%uxA2+U=t}4Ogb;VEjz}T2ZJ(}rf5V$YEym< zZH?^muH9f|iY5jVXT4NdHsfe@f z>pK?ltCcC}yzA7pYd>jgJngGs92?bx)sxUkG@ldH* zq1CiFPkDcxmGk{b3mxJ)yPWsXPFGt!W7-|O=(fG+1XY4u&>AS)P?>&nk3jJ&E59?F z@==i%UXd0;a8|f1H98)km>MR@G~Le6zUA;M>-LA0B5XQ{oOwL;k`JcaCLQtF z0aqRGQJW(_v_y3NsTyg)Yi8Vqd5Tt`)|6Vuq1S%l5Cb_)`K1{PDVzOu@CQ6?&HEJN zC?l4)9cSDHUw`Mdb-WvC5z38U50zm-`-h^dH?ydilMnWv4ZD-p%Cm>Z`S1doXm>bh zI9@$^JCP8@h|5ns)NrG^_Zy{m z^R5h4WkI`8`Ov+_4BbjIGZZ0S??1kQop(G-(DTZi^O9r456o0X0F8l_>oFZc*g)gj z?32gsdu!Y$kI)oD8PvNLJ*7e|g!X*cn zh}z=2+wsa0V?)R> z^mJ4M%Vq+()VGR_h(SCJ=KO3?#`Lm{{ji!u`MPiJE3vWtQ%TJO8%yfmN|k0C7H`E% zvadNU`7T|`spv{nSQ*%JO^s80j9||Eb~O0&@Hc5-h@x0xGrdBYCtM(*@N&IQpf$Eo zDz`csQ*-uiP1{erXT6q?GFgcpTn=;G2-bP?=0$>pwAfykwx?fJ0`zV ziT34#UCx1j&N$$a|_G6_3S-ZZzttCyDMnl5j4s(~G07pizHIHxYM zI6h@EFJ+^<5HS;Gi_#D0m-26jl(7HUob6b8J@`DHrzbjPGCSpfRQk}kIPzQD5N#$h zi&++xqXdh)>$XP`JC?k&(O9Iw)qlbiP4l=$r54V!-6i!>3Ix6gD0d5vDoNY$;=#9v z`!@r}e+f3Wu$bvkF^~(7iub&{Z_{Efcn0y%{FS093YxS8syQ9UP)^?FWen8}u#4G- z%GN{|uN4J8qUEpYj;fROwjs69&nY%vNQ=4eMW>Z%H50;(pNey{kvJZHCT`cWwkwZ{ zKIh!8UAOUA`cJ|EQ_CD+%Sdt3}YI0eFv%*jr(5Xh1&2AqFo z?|dYqKqS4PVBv`YKwylj)FT{8k{G&RSqeqhwdz0KBn^!~qXVCsQ;}LBxnj=91b^n8 zO|CDL4#2d}JAAyXa%+Pe;ZhC&&u0G>HfeO;meqDx2N?SvXMtd{=1fBVb!xb=@*yH7 zYUajq$Dp0^*20EhX;#{ZYvrrHcCFv(5eYxAUBeHqscG_gA`~o6N~!as>?eL2zS}Ir z3LxCZu|P#r?sSq>ORfL_RNgHt=}D!Gj+b1SI$Vw*<5z-W@r={;!Wqmdgy#ruWM!}_ zp*O(TyL^2&$wt!9_MD9bV>88LOw9JR*H0v>d=8thL`V++IYd>*q=sR$UJ5e!>Q9}}*Hq-wvV)6AiqbF~8cv)+vs2F1Pd0f+D-yd$3;&x$o z$O(u(Qxmj8^UM3nGr!nmX%N+^A2iM}xpuoS97XKiCiUd}>Ub*!OtvY%C1LNol%q3i zXerIZrCrVs>Qbk_nIIJKduC{b{*t!-bSxXfZL*3-AdMt$Y#($y|Mpp%M!C)m$!!E$ z_DU$7eN>DWTt_OT@y2sH5rj(~7{kW7@`(q|95#kgF*|=rjNn+)AGE~&jZ#c|RUP>T)_^g-nJh_r z!2Xltc_aKKwP!~P;|!R5_Q!{45QRHalqLZ0MOZFG0O(j)S)giyjWIRzVsOI)8<9dm zIBBIzOD)F&y;0kb(D3v!&GYIWfs!lh_vaDEd~hD1n}FHD*@<#GSJGwSd|(YL35?n= z{Ex0@ODPMzBqDs|-T^n}awOeUgt!bA24HUst}Nq$!Nn!DG(u@>{kRods6=76Zyay; zF~q+SL^+SCBXwfwM-bIq>bpK~M%WNtlZ7d&Q8CG>Lcehd;XLTB-425Vuig?cJh&wb zPQi;c2j4$@HNv&uaacCCtFT}2-;+G(o<<#>^a^pM;q@Q{z3D!VX}XV7j`jfa#l!C; zwU)6^Ynw@g#|YQ}x_vvgXcQbMj97EMG;0wRGoC8Ms%A8L3j4JMtT;Yv6 zm9UhPP_kn(Zn(4^_#X`CGpG$g-$BfP9&(1>pPj4>iYZRQ<7a9P!Rb6`yE|sYBil_Q zmA{;<)e*NP&ZT8ee@;pCqD!#JY^B%nRR zP1+#jlD>fG#+~m!UO@J5a~TvB?0amb9jogf9)V(>J$O7Q&f?ddYv7-yM8Hbkoi85! zY@cl)HXQ0*_Nu@J34?N#4qRh_4g8z^)7*GQ3POBncKerx?qd%{fjKa;%!<$6b3DLZ zEfPd19GrIjM}1|F^FxqpJk}GSB{`)6&Ruu?2(3KTwCTXoD>iXYn&uy#`??+OL^mnS zBQ(ONzf|XZYn^2}0wHsjEjaO;|Ll-HG}i}4e3p7Vmc1@-KSIyKd0T87)Y2`#r$Xk8 zR|~7ioC(z=!~^l*Rym1S(|b?Xu-yu3yP!lkv{Ryd{NnE+lIFjfcCCtiV^5yy23gO$ zLjb6F=`_oPseHM+U|CS|f;a^)Oa82emp_?UA}xQE`&tH+gb3TsLO#srgIfmkgHiTBt5 z(yqk^;p<99@|?nQwE5brGC^ z34b9BJ+0mIK6E-F&{{*PNbra_l;O7GMSC|op9IkSmlrHGOKumMd+(`5wZ<}&msM}R zvDxco#jfGixNn_8;FVsLP+a*|=D%rrak0P|BqcrrnPQu$19s#wn zo)zphcBv`{0mtwkivOK9ZNpd3?Z1tC3a(h2z+-XwCvy7ED2^G~Z}1I`d&k+1tm&TK zHOt)gZtOu2KCT9WZB_8=#qx3Xk^OKFl1@FEfvt#KZKkbB=En1O*PrY$e96L2s*NJU zcP<8Elw@u8V3E`X7vT%{Z+1UM6Tr{?rU^(@VsVLOVE3e^;b@#tCVLL2j!%J>SToi{@ce>Ff+*vfr_)C z%51Gy+Rt$IqQexs-er#v&l60tnOleflV|{|sr;DR@~I(twm!SE;osTzOP)=z)|+_W zo0@x{e`5Hlv7h#ipL^GExcDkAZEio`6HPKtvZZsw!k*MS+c&plQCD}Kb`^ITyfSU7 zvxTeNQry(+=>Hga?W8cq#fQU>;l)DjKNEUTQx8yynO0;zjVr!OxBY@R94=e_7$8H8 zi0tP<-w^-qzf+}8xZ@ikczVzZrOys;NJ{H3%c#U$nmxm1iuf1At_63}#ywEihMj@S zF7a|W&QnsYHcr*U=4|eR*?qwMHvagwF>w|3gAwkNcBq|2jXNyBz@Ye@8zZiU%t1d- zd6)H!aG%+Ttg5h3Rw5$TD}ifx#JDFl<{&`&yeDY>$T z9^*5No?pOIeP>7Ma}L}i{#iH?Y))+rO|3(^fsf@40% z8D5V5ua~lx3DzxUSO=X2wY)VH6aH5xq}pN|D&xn#wmyO1+$AR?fo5zLq6snSek^)k+(%Ac5t z9*5_FAbBW+FM`Q%kFBD(AS;D&IJ)4sbs!6q-~I(gLU-Qei>0t+gVn5cQar*$2C4(1 zg7A$!47>#BE;z4aau|o58eBNzr|6rFn~IY3jZcQAh^f*er(K;gy=0mRmQYz3)rae; zpQS${WI+N~W?X-u_0i>ntrs2R!aGb?kloKduvro4F{uQz9$g{P5%DqayzJT@|C9#; zwmgu(NP_iB!53c69WGQvwI!_kfGn$W-^Sg06y(#3k3Qeyatp`LTVu9$`<0#ZIz| zhaAPABOEfDKYyWKGV7BQB{*0Jv*UCjhQ=J-r=MrfS(|0WLPRCrw@QIQ*g>DIJ_X0b z_YAP|jz}!qwawQllLNRQEckS1*vh-@JFs2PpZ`a6bJ{`$)yw1F}~HC~!c#tO*V z=^wh(*!DM@T8TiIwqL08!Co@4%wzJGF?A1S%AbAFh7TZp3@4;C{8J*tC+FqLYC(^e zH{~8eAx%0u`d|#NC5wI|agJ_P86H%#nf+>uRJdVVg*|Dh{?u5ZOlmS8!Sf1+Wm^70 z4^Fy{9mpMgbs5G8s@y8hj*D54+~M`K*>poMB|@-64J_9R#54Hx@vHCf6i9|T2y_jU&J|YRvHynPI(x2WU>??5R zxc>oN6{Q8cly2+Di{VqM+jzPwYei?c5F*OEHD9t*cblo0gT&Dx-njzm3k89P2Bqh- z=VLZRC0t7wOANe5owGPEP2f(Md4joTnr3{>3oT{?CShUag$C(~gL#CtW+t@{4OAxQ zrSS6i01eo#kJ^yy#+c1Rg#c zn7ZFh-d!viK0i1( zRNDFs;>ZA4z2|-WRqLRVej{q!OM1J~!TUYdkI?GFrJ5XmL5nv^juc-eVL}TNOLM{) zWS4I-4CL&A_O;`?)0A9)l4TDa)LWan5G?6G0+%7wW9+@vp8!_VD=XF*bZzymOlpLF z(DZXMZpcN7p%L)RcHzR$d7wHR%UOrYF@dCQP1qMeYLK`s51~^GEU|1haCK=Mu zcGjE!DGHGhyyUq5`Y@qgTr}@%d!x)7qKTvnQOzHZ33-o{H`tXhKoSj*8#% z%Fj+Vi&+pmeUA~~@z&zhT-&*q5iAo8QZmEj5*4HGpSkQ?0FvMAaNuU2Ky*!;fgYAz zY0xy51Wz<7h8i^-SLsCgL4P-1{cco@*|M|4AQ-*e@Wgra2rqSNlu`dbS0$OEiVWJ; zo<%o|oemB3u&1u<7eqX#vD>&lWycrFaK6eX8^?M88i=!_EtWXIEPh(nVT0)uj- zd_8lslgYDj44w0-=D=-t6m^4u!o_6N9M~wUuX`;HZft?kf`)f^%P)wKN7z`Vo&!%+ zFVdoNhI!}oUsvR1YaZ>hI4j9BlgpJQ3w1B~q#EKc--mxl=fks_8X7Pg^=GID&*hPM z48s$6Y9dp#A(s)@sP+7ZWDyzsIJQ`nNkp_=fqxAy+M&yN1stJzo-CgE`bdkfp>XKm zOQvq&fE>ASQVKu8+?j0?I6=>f38b}|+bp$IO<)UcYrHV%7%nbQ zIm>TeN^njHp$U}`$fO302Cp|OR3XQ17;{RK#cpr_Ldxy$qSeB5JeWMtl^Hk}`f~Ey zX^G%uA0|%fzwGIh3&(M-5^#}9byv$5IKyRl$;nQJKmr8PCEGv1JEISaKl-_X8kJ+1 z>vWqmN3i+}82*-e(x&AyXQ@sXe~8;CJxwIT=Mko+Ij+Fz%CdTrCj>7G?*Rb3 zX?rrDv=li-VUH%pUMy$=gw{Ma_3^rpppa0ORrNVT#P9e+7x5YJR+6#1a=TWV_ffMA zo#nl#n93B<_WLV1*)O*xI8oWk-uwZ@NYk`(&7?Ln78PG=`N2guJZw zWD};RE!>H?jq!6#Mf>JxHSOQ+kwfhG&O(!y9<;^n;r1(gC4Aj=Kc5{GWY;!X!hs`l zZ--dC9Ck@Wk59d-G9a`&Fg>09j&+fHQO7nGo-$2Frx0J+07YeW5!LL8I?y*PGl8={ z-el#$2|P{xe$czzx?$3gjRDhh6SJr>#iN%OM|ic>GLF>rb6*^w)VLolNDbTkq8@Lt z^IJ-WHi4QPCNd-hKR4@h2$uo;`}e4C=}t#E1@5CKaF<)_d*&`lG2P=)-!`5CT6{WND|>tZg7Akc!?JW^QzrKT{+brTtGRsJl1RUcXp`j)}U#_z^QA#e12( ztC@58Ce`MT&6tC7ftoAID}_y>;V*EU_nYEki7uRd zI_8BbqqVuJF7qpQn(}P%FCW)W(qQsJDL-0CSWx==Rw5?FiSxvTQkcJZlEWj=>q?u1 zF(1?iozg$D2cjvx-hbB6`wB!@)#!J-M<}xLpR-VV(fw8Q>~p`z-V}7FuqUK3QP1$x z|H{rMUnah5OVnCqMsp-~(gmO2Zmkay8^)DZLAj>qx)tb{PZxG<>)Yy4o_r({u` zlE`MlD`=W1Yum(Rh^yk^;)-Ugb}VkiagB@JG69ieAeP)D8Wj{loG9w0GwJP_F+Uzpd#Y$_Zs( z`&5>kL@~04Nz@5(FxJYFwK3LYN{YtbW+#oM!Jw2emL!IAT8J#!m$Elf<4}&{^S*V? z=jc4Xf5P|raop~?=DM%@dcUvN>-oO#jIJ8r=_|3COrtKHXYY0_YcS_8*jN`T8RTVj ze)$NGB^cHcldm_ZDss0!y0c25;q*&_!`%3EC=U3UBSNAzmFzNOCa&29TMJWOS?RQ8 zW9=}D5i^ZX>&eM0Z-XMct0ik*@5__A5@D-*QH4Dx@!il0UTs&P)R0XuNcAT=cFk&q z!8Kg*q0ZF!%1qDQ|J>p0Eo;r@;?xy9_;_aZ4nzR!zay6*m`$N~q2VzpS#k$Z);* z>}Z+DmOrE&VwO(bP+?7G2Nbv~aXL)a3m8+FA~s9>^3Q95%fW&5UVd z;7Q|*B6ruhcHvzV#`e!t?xDeOui8(~Fc-CztbNE1wjv!zIJE;_7DP1#-YhL=^XBJ_ zk;?D+e5vgzw;?FuYIN|Ns#BLaPn1Q$CS=`xqxf>o!Tk?;+Tqt0!C%++nvkc_fP8co zOzj;+40Ice`CPZ%T-kJjbU4~#ax7bVacbTwPbk9tAhvlSDyegrJ_;|}Nh2Fr`my(wG%hF6dUf*BhyU8$k$V<)%b0H!Y2XB0YSr zTe74B5(D>zY|34=K!tylKoTOjl}Caj)H>D(5wgjG*8i7;MHA3>&0W%p90<+`5a9A! z3^Emyq(vgfAKz4;=`?^6rQxvcFjZo5g)yBmx#I1XR|>ogY+Oq`WKECRjAdBJn}20x z{%-5*)^==wlzn}^DZKrbXb+#P=^u#%HZV%W>F&5qM90t^a{jM*=fy+!WBrt=t2~8Q zWyUH*X^vGlS=Ye=YiM@_TEo_VSV{Ajhxs!#QVpG%^p!4WgQkx8m6im8gkDRgNa%z> zXu!@v%Xe=9@DQOiZVyR^w7`+>&v1s!R%!Ycg_)1g=l5T_ZwLFX27gF0{!^b=7Osj? zA872Gbxnsnu@_{0RC-z!lyUV2u#gw)R62{K8zs@)0jlBJiV(tXkk2HO^M}_>U&{kO z_cM}OL0E+?Pt6wAFpylXVCX|SUQxaoDc2*?T#jTb10{CkAZN&R0YgA_vo!m^W^S&0 z*(2M=Eg@@E1)0KLja;1`i1Q*B1vL(94hSE~-VQdFyo7BJ`^c}iQy#<9RSpD*Stgdu zHsq#e=Jm6wDntZlGzdrVZU+GoT!M)|mv9t2xm}nz;~zsGt7h*~gJ=JQ1N>8+D$a4D z>`ux>P!haQ&wM*{&Z|eIQ>TZk9>w9Bey;72XijcvR77$;&9+KwOg75+=r*%^knoWC zxYz09FG)Kog>fvs4d6YiS6J$;H?H1N7+y;0hcrvjjCq!*2S{LT~-&SH;n>B@`|$@V5i@sKDKk^m0f`J_C8UraE;s4z$}0Xr zP^3G0#1{}4tN4smBKMwMRJ^9-$fi-0t{@;tUU>}Vd<`zN8g6s*`7t0P&)$Rk) z;hWp-OsJMBI~%!4PtRfErhu<4$?%2vjG3i;`7Kg%_|yD8l^Nm7O^FS?ksrCY%KzB{ zZaElgZUI(u#IV&__PThIoS#$2xi&gGv`v;9QGW+Hp$HhXLbATP z2~_TjKvo`!35j3^FP;*&NpR7!A+&+d%C3GF+;+{g-eCz9ejrEN(wu*nt{!RLYyG(e znZp(B?1cU4j-8qbm~tvd)C1-0Va*Tul=gQsZFg?v%&zIO-<~?4`Mdi-q9c??2?DXv zG$Nr_cepxXG9Eve6ugr@;0e+{cr5~3V1nLeSq7T?0xspA1WbY)2jblkO&M2p;&Ztc zAO%?g2++~JRwS3c-Jf2#e||Pz%K1Fi@LAGaxyuiRAPe552u@Xl?)LjP-3^@YQc}f< zb>Y?2+myYe4nVu6R7OIEp_?RkynoJO@-maFT}eYN7oNo~U<%#{R%(5}GoMoFVCw+8 z(rR)kztco3f#3&I5f)PP=0^A0ki#Jj&JF}A$?c(xf?JZPEy>U(kI-c zaKq-?pmfsQktc?^{j!{29sc(7Vs72y$JX8nY4GcEveP|%y(f(Lshk?FvMRQCbuMk$peTtjv z^x*-wy(U|PelU`XAS{)qK??X_n^ONa635gXe84+R|7S;eSj8|%OQ@;_w?lWdt;`8N z^|OhSxk2a0k6DE3%MCoNQB4P|vh}x!$wsEJ0O8-S>Le(K0%if z)TOB~;GK&Qpchygh1K>7{gZl^W3K$TK z;}6e~ZmBix-Kvgcd)?O+Yjp2!cGh9e&y=Fws*;IY5}yegW1NRgNBdzAb#TkC#bBgf zNeD%{qCxEoc#3aO2i%;+x&%wa-kwKwKV$8HwiQls)QtFWyFXTLz8%bHy+WLTKwBxV zK{$=rN`|{K&uw#GV)W@`#)8ip)VyUg zSmh~TmD>!X8tGxoC#Ra2Z&3Ry45R?^t0%keazsKFur4LFV+$#C=2l5YAGoN&BtC6f z(U>5Y(b{|}LFDw$;y&joGxhqLTyqs+^|uJ$ z@*pN)@U0jKbyRW=Tvs)eH6y;VqontSk6vU14nDK2rs8d$>RHTRQV@Qn-AE7_C~#A- z8*x6P)D*!{lvXpG$Xe#+rcK{i*RCo+`*b}czM${z)l`o2xf%w>%!sBqD_I+NEk4!b z#nJM_f-qZnYN}+qoycWhm~eDv^ZWPt1b&a?Z+;}|TSF#iJa{nz%R4yuVX47~j0P)9 ztkd6jBqXTmu*h2ta1bXCrl>3Ii&F$;cCV+RMH294W_|C6x+5SaC`gc2xHaYtx+0=f zu!$fAI&^5me%<++5GEVUesJr%39N>b9jPn5!{Ec;QQz*inKfKZ-n>K{1?kAX!(m|Z z>)4ysXk3_T9p^q^#w+pj+Cp?{5$!ie5ey)Yu^?&)`QdY*y?gZDN?u zFN8Gnj`K3pi;p5}!J8}j;ss{0`02=Ng^e+s*DG8j5t;K2*Go~Qw5f* zX_Gq^_^zN@%)n~={7H2npJT&^C4lnVZ`0P(?|u4NK|q`So%UqWrSq4n>OWA}oM-=* zU%-1k`+5f`_D%G z94{yeDe6La-;mx|@+zvZN3+b2*I;$HBx27stx!&dLuX1z9{V-3Q?D0n_bALK8)C1$ zSR7FUd&f*m7}r}<(1|mLl*)QB?$ru)EgQp_o3?$@nKMYuGYoL&&y!Qrcg293l17VV zE9!Zjq}8tIdFbSJ*$Fn5ABy8amQoHSO&mdYy-o%hOLIrOPk4{{(M{J1R|TZ8+Y6F* zwP5>Q@)9FISEv4H`ZoI8l57=rT`v8x^e_4}Wlt5xWr8h5%PYO&%}$49-#tv9ay;3K z?;%*YgOeIQZPtVmla)o;xT&GwYqDaUnjpR8tgOE2bZ>RM5L9#BoG%NuaI`Vd=7QKx z6hi3(qx*=r9VoxpVWhjj~y;@UMJtYubE8s?GTg6on`Fg(~KS~GOc=>^B*<{?1~Avx1H6QDEhR0?YDK^E6b zF>po_zz-Hj3T$gzJOCQ%G8+_i*-aR9r1AW~Q3^|-k@bnB1#+F7_0uPi7d7Ap#=g|k z+$(=gri}2rQG{o|T{T}pJli9&CIjD22WOYDi&F)Ik(ob5J4$vIS?aiWL>z2|TPjy*rOL2_$(z0h^be=zct0C4PRec%j)gPK|zgw}|kmN0%}i_HjS;c{3z3T-`>X?m5ks@f(OEN%H*=JY!zynQOrr z8*nY3GBSTFdL?;fGFpB?ns?;4qqhD^8V^gy9U|#ITQ#1J__u{Tz+?9ZoHs1{Yr2W8 z2LS;PRNgsQ=cW>Gu!;O6S@wru-?^dte`cy5s8Z(gC_E=7+p0Oh3h&ehQ?t976)(18 zZdJDyBKc%`rh0|Oe|NN>wLYM2C09th#vJit>FgMIS4_4tkZU}RNKp?~JhZ0!NXcHT znc|;*RbWy#LfV25WzG*CXQVtnkeT2ja-K9v5pEZ`-HhNKoNp5?ru*@YW(T}?=!zIW zHlPj9;^z_Z>g~>%yphbKSu$?WCHnrDZ>@o|WKm9k;Gt7%nubb??nC59mtM;JvYD3H zwPyarrfmq{IHUJ)!1h9k%fLBmH_yTN@y$Hj?g83tAs_LO%Ar@52{jHrxF*At%7I&~ z9N?7jGM?HA816HGPAX3b@1+WVHenuj`e3K7ZQYf;dW>qRE%6u;^L_0?4jSH1o=FS5 ztGi3B}yz`C{b#0$9Tc6+iZ1wJ(m7g=TZa5CQ zO}H}2!OW1gM`t)bcI_q=w+(xTv1zpuES{u)7AwY$GmFDtHm<&CJ{Tqk-PM4ws}NCz zc2`LuzHg^#zT8Tw#dxcjJ>U=<+K3BUJy#ee%p69m(r`@_h{!32fz9~I1d>QJ$KTM( zvmtcC!S-)NwyWQ{HivdO^9KmKXC_EX><>f9mct4<&5I5%tO z<&PFm&5Y@+;JT;(3RnLejvyi8;7jilq>`ABk9hf?e*HybePuFts^S=8BnO`b$9zL0 zwyePB6Vcm0)+%PE^?-VhTMG9dx*GiQf9iNP9?pc|gz%v{Io1jF=cCcJ?z*`n@B^`A zzl>VrO*kMiP!?-LYxoS|7vWX}#g;69n&sb+T63H<_POC!*?Q(pNE9}-c7Z^7ZsH|S zuI)SE7V&xj@MrBS5g=$(rg2$iZ#kUCTY+8w?mP5!AO$1Wi z7_w7o-=g}4Bj8!-Y z-`M8rXtlMKRJM?t^2%-UK&?R$ga;~I|LeHEO9@R=nZRf&WZCwxArcSawYnS6)367= zjSL$!1yKIr@8vfuVX=h-eB(jLhfialK6TK!M}CFKYx?yp)FxKGS-*pRKntWdv`Ua~ z10C0A8G1ft{7xwTN0;sm?ZYBE0(f9x6{i6VhdPq>Z6;7%u;CB{a|QZ+3lN_aM|(KW zRUpRKVXh6w1btotVZ}5|!PC=zGxBEQFNSX8QGxy6)JY;k-ZNBjIv%Xok9^_4 zqJFm`cQHSnMG*G4C<_2kc0`~&BNxi3l&4$ahlb=y7&L7>Co*G@BpLT}g(Dw% z%&_(--OBsju(wNnvwV?b;D+p-)}7TtM%dO((Y?S_Z9HE)SdeNSQ1fT@)dfa>U{pP! z_TQub6<({ztqQx8j(Ye?|Cb=({Cj?WC2f4q`1%*@(_6EJ_uG8mazi0heg6@0olp7x zGd@9BUcPV6L|9+H{|MoL#P?qxiAqEG`MxzEKpKAk5yBnwfAshNyS8DJ{u#C9$eJK& z^e@t_!(mAzxo-!FZrgAnolQ54Fi{4A?f=?UcHRD>`j^V;73fFUBNj$f1Lw>C1!;Su 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 9d43ff17..00000000 --- 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 0f4245f9..00000000 --- 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 47f9ae24..00000000 --- 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 9a9ec937..00000000 --- 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 9e264ac0..4bc7b0ad 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 4fd02195..00000000 --- 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 209e3ef4..00000000 --- a/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -20 diff --git a/CREDITS.md b/CREDITS.md deleted file mode 100644 index 6dd2d893..00000000 --- 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 1e9ecebd..00000000 --- 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 25778667..6cc99aff 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 b850ba72..00000000 --- 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 84c5f04a..00000000 --- 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 2650e9ed..00000000 --- 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 55102ce9..00000000 --- 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 af25adc5..00000000 --- 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 f951f42a..00000000 --- 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 774a862a..00000000 --- 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 00000000..a5ee6c82 --- /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 f21146ba..00000000 --- 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 00b88008..00000000 --- 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 7bc3773f..00000000 --- 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 436d8a21..00000000 --- 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 df2595f5..00000000 --- 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 56994a62..00000000 --- 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 be32c75d..00000000 --- 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 6efc7ed3..00000000 --- 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 c48ee738..0e1deb8b 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 5ed4d6a0..00000000 --- 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 87928a8e..00000000 --- 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 7524907c..00000000 --- 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 def75dcb..00000000 --- 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 2230f73b..00000000 --- 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 39c074a6..00000000 --- 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 d8a879c9..00000000 --- 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 37c67a23..00000000 --- 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 905e39c9..00000000 --- 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 2883dad9..00000000 --- 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 45f88d12..00000000 --- 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 06e52a93..00000000 --- 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 ed4e59dd..00000000 --- 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 45db9d7b..00000000 --- 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 0ece9553..00000000 --- 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 c091590a..00000000 --- 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 db450d95..00000000 --- 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 00000000..6d5ea554 --- /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 00000000..e2c1ccc4 --- /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 00000000..40f79c4d --- /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 00000000..15d66f45 --- /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 00000000..076e3857 --- /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 0637a088..00000000 --- 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 75e12f22..00000000 --- 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 02e4a84d..00000000 --- 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 fa7f1f7e..00000000 --- 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 4dfbd3fb..00000000 --- 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 b49dd2a5..00000000 --- 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 5d3bf41c..00000000 --- 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 6b2edc55..00000000 --- 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 12e05724..00000000 --- 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 16c9ae19..00000000 --- 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 9c10a599..00000000 --- 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 ee062102..00000000 --- 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 0f15e166..00000000 --- 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 00000000..dcca5a9a --- /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 188542f6..00000000 --- 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 414b2762..00000000 --- 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 c01540fe..00000000 --- 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 dc64af25..00000000 --- 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 a929a1a3..00000000 --- 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 1f231aa6..00000000 --- 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 0fbd15de..00000000 --- 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 3db14f64..00000000 --- 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 d734a682..00000000 --- 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 5fe007aa..00000000 --- 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 1cb2ebe8..00000000 --- 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 3cb4811e..00000000 --- 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 336ddedb..00000000 --- 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 370dd892..00000000 --- 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 4aa6a133..00000000 --- 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 8dde5ce9..00000000 --- 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 34484535..00000000 --- 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 2c789f6c..00000000 --- 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 2bc9731c..00000000 --- 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 b352ca75..00000000 --- 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 171ef095..00000000 --- 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 5e806df3..00000000 --- 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 00000000..48ca11a5 --- /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 03549bfa..00000000 --- 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 00000000..085b19e4 --- /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 93c4610b..00000000 --- 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 00000000..993ae386 --- /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 723afa47..00000000 --- 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 d08ae511..00000000 --- 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 fcaa7983..00000000 --- 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 d4adc466..00000000 --- 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 00000000..c4160001 --- /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 13b754bd..00000000 --- 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 16150293..00000000 --- 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 8f9b9ae8..00000000 --- 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 f507796b..00000000 --- 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 edcb2ec5..00000000 --- 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 1f4bfb4a..00000000 --- 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 e30f7e0d..00000000 --- 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 a1749d1f..00000000 --- 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 c784b85d..00000000 --- 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 6ce14979..00000000 --- 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 f0352fc0..00000000 --- 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 cf5a78da..00000000 --- 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 b122dfe2..00000000 --- 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 45c72553..00000000 --- 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 d5eed20b..00000000 --- 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 36fd70a4..00000000 --- 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 cf0c18ab..00000000 --- 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 71e0c8a3..00000000 --- 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 5a4fdb7c..00000000 --- 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 15ed98c6..00000000 --- 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 31c92d4a..00000000 --- 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 d279475e..00000000 --- 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 5a45505b..00000000 --- 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 0bb0a4e7..00000000 --- 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 ff770888..00000000 --- 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 992f9638..00000000 --- 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 2fd67bd5..00000000 --- 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 49717f90..00000000 --- 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 fd5d71ed..00000000 --- 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 d9be3a02..00000000 --- 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 62b37d3a..00000000 --- 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 811427a1..00000000 --- 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 aac71b3b..00000000 --- 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 e1e7216b..00000000 --- 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 440e0916..00000000 --- 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 1eb7575e..00000000 --- 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 a8f0b42d..00000000 --- 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 52dcc256..00000000 --- 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 7ed90d9d..00000000 --- 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 66d1f74b..00000000 --- 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 c4f6f4c0..b95e7e02 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 02/29] 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 00000000..f965aed1 --- /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 a5ee6c82..cf94952a 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 00000000..fdd42344 --- /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 00000000..39da6d6b --- /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 0e1deb8b..d7548382 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 6d5ea554..6ef778bd 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 076e3857..1675d230 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 00000000..8675f79e --- /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 dcca5a9a..576d42a1 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 085b19e4..00000000 --- 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 00000000..ed2e5ff9 --- /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 00000000..5bb9da20 --- /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 993ae386..00000000 --- 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 c4160001..5501f09d 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 00000000..e5294bb8 --- /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 03/29] 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 cf94952a..2c9571a8 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 d7548382..ff72d906 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 6ef778bd..6c0a62b9 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 00000000..da174032 --- /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 e2c1ccc4..00000000 --- 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 00000000..3ead3a6d --- /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 1675d230..c704d0a0 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 00000000..93e0cdbe --- /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 576d42a1..06e500d6 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 00000000..41262c85 --- /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 ed2e5ff9..eb53fdb9 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 5bb9da20..95e4a8b1 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 00000000..ef7b7205 --- /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 5501f09d..a8cae1c5 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 00000000..d39ccbf2 --- /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 e5294bb8..8d78ae2b 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 04/29] 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 ef7b7205..09770dce 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 05/29] 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 09770dce..06ea1be2 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 00000000..a9712473 --- /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 06/29] 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 c704d0a0..b68a02f2 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 07/29] 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 06ea1be2..f1e6e684 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 08/29] 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 4bc7b0ad..138ece65 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 ff72d906..515c3010 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 6c0a62b9..12c7f812 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 00000000..14d4118e --- /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 00000000..e39d9375 --- /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 40f79c4d..26d00e5b 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 00000000..0b2f9357 --- /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 15d66f45..2a9b1313 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 00000000..fa19520b --- /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 b68a02f2..a173cb76 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 8675f79e..00000000 --- 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 06e500d6..90e7367f 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 00000000..1af70f8a --- /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 41262c85..a77d2b44 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 d39ccbf2..425fa460 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 09/29] 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 6cc99aff..eb71a1e4 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 12c7f812..7c60a5dd 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 14d4118e..e4adf9ce 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 e39d9375..e64f31bc 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 00000000..f8cd3527 --- /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 2a9b1313..614604af 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 48ca11a5..a9ed6acc 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 1af70f8a..cf7c376d 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 95e4a8b1..d85bfc1f 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 8d78ae2b..522762c2 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 00000000..a4604337 --- /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 00000000..9994ea67 --- /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 10/29] 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 2c9571a8..c03c8c92 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 515c3010..c6f6f741 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 00000000..a0169cf7 --- /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 7c60a5dd..bb43497f 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 a173cb76..013de16d 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 90e7367f..8758644b 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 11/29] 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 eb71a1e4..656260be 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 c6f6f741..d8460bd4 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 013de16d..82dc7894 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 8758644b..3a4521f9 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 12/29] 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 00000000..428e5953 --- /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 656260be..9784f980 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 d8460bd4..d28b22ad 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 00000000..5b84757d --- /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 a77d2b44..b57fbf7f 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 b95e7e02..ab566ad1 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 7d6fafe3ef8d6c5709b44e235d59c6e741aba0ef Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 11 Mar 2025 22:14:51 +0100 Subject: [PATCH 13/29] 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 f965aed1..152a6192 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 138ece65..964a9182 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 00000000..4d9290cb --- /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 c03c8c92..f91ae698 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 d28b22ad..06509479 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 a0169cf7..39b107d4 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 bb43497f..027f2ebc 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 da174032..3d5baff4 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 00000000..db8b6bb5 --- /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 3a4521f9..a2dfb814 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 b57fbf7f..14006ebc 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 00000000..82cdc9d4 --- /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 425fa460..7f9afe61 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 00000000..a554c21c --- /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 14/29] 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 152a6192..1e140910 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 964a9182..aa426ba1 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 00000000..133b8673 --- /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 15/29] 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 9784f980..ad1371b2 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 16/29] 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 133b8673..2ff5edb9 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 17/29] 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 82cdc9d4..3108e0cd 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 18/29] 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 3108e0cd..f113d14c 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 19/29] 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 14006ebc..5be4dfb6 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 20/29] 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 5b84757d..3872d561 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 f1e6e684..e0406459 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 21/29] Fix: Adjust Readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ad1371b2..ed73de8c 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 22/29] 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 ed73de8c..eae97c47 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 23/29] 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 eae97c47..3ea81873 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 24/29] 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 26d00e5b..db77bedd 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 25/29] 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 5be4dfb6..e6185517 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 f113d14c..600dec55 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 26/29] 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 027f2ebc..899f7b7f 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 7f9afe61..3d95b353 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 27/29] 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 00000000..3bea0446 --- /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 899f7b7f..56543cf2 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 82dc7894..b35aeeae 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 28/29] 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 3d5baff4..ee0d7147 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 29/29] 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 e0406459..43c8038a 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