├── .env.example ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── bootloader.ts ├── docker-compose.yml ├── index.ts ├── package.json ├── scripts └── webdav.reg ├── src ├── DICloudApp.ts ├── HttpStreamPool.ts ├── Log.ts ├── file │ ├── IFile.ts │ └── VolumeEx.ts ├── helper │ ├── AxiosInstance.ts │ ├── EventPatcher.ts │ ├── MutableBuffer.ts │ └── utils.ts ├── provider │ ├── BaseProvider.ts │ └── DiscordFileProvider.ts └── webdav │ ├── WebdavFileSystem.ts │ └── WebdavServer.ts ├── tests └── raw.spec.ts ├── tsconfig.json └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | #Token obtained in bot control panel 2 | TOKEN="TOKEN_HERE" 3 | 4 | #Guild (server) id where your bot located. For example: 123456789012345 5 | GUILD_ID="GUILD_ID_HERE" 6 | 7 | #Channels will be created automatically if there arent. 8 | 9 | #Optional. Default = "files" 10 | # META_CHANNEL="filebot-metadata" # name of metadata channel in your discord guild. 11 | #Optional. Default = "meta" 12 | # FILES_CHANNEL="filebot-files" # name of files channel on your discord guild. 13 | 14 | 15 | #Optional. Default = 3000 16 | #PORT=3000 # webdav server port. 17 | 18 | #Optional. Default = false 19 | #ENABLE_HTTPS=false # enables TLS/HTTPS. 20 | 21 | #Optional. Default = false 22 | #AUTH=false # enables authentication. if disabled, all users will be able to access files. 23 | #Optional if AUTH = false 24 | #USERS=user:password # if auth is enabled, users in form of user1:pass1,user2:pass2,... 25 | 26 | #Optional. Default = false 27 | #ENCRYPT=false # encrypts files using password bellow. 28 | #Optional if ENCRYPT = false 29 | #ENCRYPT_PASS=123123 # have to be not empty. max 32 chars, empty and not existing chars being filled with 0, rest will be truncated. Maybe will changed later. 30 | 31 | #Optional. Default = 2000 32 | #SAVE_TIMEOUT=2000 # timeout for saving data. Bigger values may little bit speed up uploading but if the app crash - some of data may be lost. 33 | 34 | #Optional. Default = false 35 | #SAVE_TO_DISK=false # Saves the file discordfs.json in the temp folder in the os. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | out/ 3 | .*env* 4 | certs/ 5 | yarn-error.log 6 | logs/ 7 | 8 | .local 9 | 10 | .space 11 | Spacefile 12 | index.js 13 | 14 | test.ts 15 | .vscode -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20.9.0 2 | 3 | WORKDIR /app 4 | 5 | COPY package.json yarn.lock tsconfig.json ./ 6 | COPY src ./src 7 | COPY out ./out 8 | COPY .env ./ 9 | 10 | RUN yarn install --frozen-lockfile 11 | RUN yarn build 12 | 13 | RUN mkdir -p certs 14 | EXPOSE 3000/tcp 15 | 16 | ENV NODE_ENV=production 17 | 18 | CMD ["yarn", "start"] 19 | 20 | VOLUME [ "/app/certs" ] 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Ultima 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Contents 2 | - [DICloud](#dicloud) 3 | - [State and details](#state-and-details) 4 | - [How to setup and play with this](#how-to-setup-and-play-with-this) 5 | - [Discord server creation](#discord-server-creation) 6 | - [Bot Creation](#bot-creation) 7 | - [Setup](#setup) 8 | - [SSL](#ssl) 9 | - [Encryption](#encryption) 10 | - [Authorization](#authorization) 11 | - [Last steps](#last-steps) 12 | - [Known issues](#known-issues) 13 | - [Limitations](#limitations) 14 | - [Contributing](#contributing) 15 | - [Disclaimer](#disclaimer) 16 | --- 17 | 18 | 19 | # DICloud 20 | File manager that allows you to upload and download files to and from Discord and manage them in various file managers via webdav protocol. 21 | 22 | Yes, even ***above 10MB***. Currently tested about 750MB for a single file and 9 GB in multifile mode. 23 | 24 | Supported functions: 25 | - Manage files (Upload, Download, Delete, Rename, Move, Modify) 26 | - Manage folders (Create, Delete, Rename, Move) 27 | 28 | # State and details 29 | Not even alpha. **Created for fun and ONLY for fun**. Dont use it as important storage, since it *active development, contains bugs, LOT of _bugs_ and im still making LOT of breaking changes*. Use it only for testing and playing around. 30 | 31 | Please look at the [Known issues](#known-issues) section for more information. 32 | 33 | 34 | Has been tested __MAINLY__ on Windows 10 and on some third party webdav clients: OwlFiles (Android), WinSCP (Windows), dolphin (Linux). 35 | 36 | 37 | # How to setup and play with this 38 | 39 | ## Discord server creation 40 | Create a Guild (server) for the bot to use. Save its id. You can also use existing server. 41 | 42 | ## Bot Creation 43 | Create a bot with admin permissions and invite it to your server. If you already have a bot, you can use it. 44 | 1. [Create](https://discord.com/developers/applications) a app. 45 | 3. Once you created app, you should be in the app menu. Go to ``BOT`` tab and create a bot. 46 | 2. Once you created bot and you on bot page, click to ```Show Token```. Save your bot token. 47 | 4. Once you saved your token, scroll to the bottom to ``Privileged Gateway Intents``. 48 | 5. Enable ``MESSAGE CONTENT INTENT``. 49 | 6. Goto ``OAuth2/Url-Generator`` tab and select ``bot`` scope. 50 | 7. Scroll down to ``Bot Permissions`` and select ``Administrator``. 51 | 8. In the bottom you will find url. Your url should look like this: ``https://discord.com/api/oauth2/authorize?client_id=YOUR_CLIENT_ID&permissions=8&scope=bot``. 52 | Copy the link and visit it. Follow the instructions to invite the bot to your server. 53 | 54 | ## Setup 55 | 1. Install [NodeJS (Tested on 16)](https://nodejs.org/en/) and [Yarn (Tested on 1.22.10)](https://yarnpkg.com/). 56 | 2. Clone this repo. 57 | 3. Navigate to the root of the project and run ``yarn install``. 58 | 4. Create a file named ``.env`` in the root of the project. There example file ``env.example``, so you can just copy it and rename to ```.env```. You should fill the file with your data (token, server id). Other settings are optional and documented in the file. \ 59 | __If you dont have opportunity to use .env file, you can set environment variables instead, they should have the same names as in `.env.example` file.__ 60 | 61 | 5. To run the bot, run ``yarn boot``. This will compile the project and start the bot. 62 | 63 | ## SSL 64 | Warning! At the moment SSL support **is not complete**. You can use it, but you have to be aware of potential security issues, since TLS_REJECT_UNAUTHORIZED is set to 0 because of some temponary problems with requests. \ 65 | If you want to use SSL, you have to generate a certificate. You can use [this](https://www.sslforfree.com/) service or [this](https://letsencrypt.org/) one. You can also use your own certificate. 66 | 67 | 68 | 1. Generate a certificate. 69 | 2. Rename your certificate to ``cert.pem`` and your private key to ``privKey.pem`` and if you have chain certificate, rename it to ``chain.pem``. 70 | 3. Put your certificate and private key to ``certs`` folder. (You have to create manually, its included in ``.gitignore``). 71 | 4. Enable HTTPS in ``.env`` file. Set ``ENABLE_HTTPS`` to ``true``. 72 | 73 | ## Encryption 74 | 75 | Files in discord are not encrypted. Because of this, the server supports encryption via __AES256-GCM__ algorithm. 76 | To enable encryption: 77 | 1. Set ``ENCRYPT`` to ``true`` in ``.env`` file. 78 | 2. set ``ENCRYPT_PASS`` to your password. This password will be used to encrypt and decrypt files. \ 79 | **WARNING**. If you lose this password, you **WILL NOT** be able to decrypt your files. 80 | 81 | 82 | ***WANING***. Unless its look to work stable, encryption feature **still being tested**. Im not experienced in cryptography, so i cant guarantee that it will be secure or stable. Use it at your own risk. 83 | 84 | ## Authorization 85 | You can set authorization for the server. To do this, set ``AUTH`` to ``true`` in ``.env`` file. 86 | 87 | Then add your username and password to ``.env`` file. Set ``USERS`` to ``username:password``. You can add multiple users, just separate them with ``,``. For example: ``USERS=username1:password1,username2:password2``. At the moment, only basic authorization is supported. 88 | 89 | 90 | 91 | # Last steps 92 | Once server started, the webdav server will be available on port 3000. 93 | 94 | Windows explorer will support webdav out of the box. You can now [add windows network drive](https://www.maketecheasier.com/map-webdav-drive-windows10/) to http://localhost:3000/dav and use DICloud as a regular drive. 95 | Or, just use any client you want. 96 | 97 | # Limitations 98 | 99 | Does not suitable for low memory devices. Uploading and downloading uing in-memory buffer, so it can consume memory. 100 | 101 | # Known issues 102 | 103 | 1. Problems with downloading big (~50+ MB) files from **windows** explorer directly. \ 104 | This is *limitation* of the windows explorer, which limiting downloading files to *50MB*. This repo contains a registry file, which can be used to increase this limit (up to 4GB). Script may be found in: ***scripts/webdav.reg*** \ 105 | If you still having issues with this, i recommend to use [WinSCP](https://winscp.net/eng/index.php) for downloading big files. \ 106 | **You can also** download big files directly via http. For example, you can use this url: ``http://localhost:3000/file.ext`` or ``http://localhost:3000/my/path/to/file.ext`` to download file ``file.ext`` from root folder or from ``/my/path/to`` folder respectively. 107 | 108 | 2. Uploading and downloading big files (~1GB+) is working unstable, so if you really need it, split big file into smaller chunks (lets say 100MB, with any archiver like [7zip](https://www.7-zip.org/) or [WinRAR](https://www.rarlab.com/)) and upload them one by one. After downloading, you can merge them back. \ 109 | Current tested limit for a single file is about 750MB, but i guess it depends on your internet connection. 110 | For now no other solution for this, sorry. 111 | 112 | 3. Half-working SSL support. You can use it, but you have to be aware of potential security issues, since TLS_REJECT_UNAUTHORIZED is set to 0 because of some problems which i dont know how to fix for the moment. But if you dont care much about targeted intercetion of your data, you can use it. \ 113 | Will do some work in the future to fix this. 114 | 115 | 4. Not all webdav clients are working as expected. Will do some work in the future to fix this. 116 | 117 | # Contributing 118 | 119 | Just create a issue or pull request. I will be happy to see any feedback or help. 120 | 121 | # Disclaimer 122 | I am not responsible for any consequences, data loss, damage, law violations or any other issues that may arise from using this software. 123 | 124 | The software (probably) violates Discord's terms of service, so use it at your own risk. 125 | 126 | USE IT AT YOUR OWN RISK. 127 | -------------------------------------------------------------------------------- /bootloader.ts: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | dotenv.config(); 3 | 4 | import fs from "node:fs"; 5 | import root from "app-root-path"; 6 | import { GatewayIntentBits } from "discord.js"; 7 | import DICloudApp from "./src/DICloudApp.js"; 8 | import WebdavServer, { ServerOptions } from "./src/webdav/WebdavServer.js"; 9 | import DiscordWebdavFilesystemHandler from "./src/webdav/WebdavFileSystem.js"; 10 | import { getEnv, checkIfFileExists, readFileSyncOrUndefined, ensureStringLength, withResolvers } from "./src/helper/utils.js"; 11 | import Log from "./src/Log.js"; 12 | import { v2 as webdav } from "webdav-server/" 13 | import express from "express"; 14 | import { IEntry } from "./src/file/VolumeEx"; 15 | import archiver from "archiver"; 16 | 17 | 18 | process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0 as any; 19 | //Without it throws error: cause: Error [ERR_TLS_CERT_ALTNAME_INVALID]: Hostname/IP does not match certificate's altnames: Host: localhost. is not in the cert's altnames: DNS: *** 20 | // Idk how to fix it now, so let it be just disabled. 21 | 22 | 23 | export interface IUserRecord { 24 | username: string; 25 | password: string; 26 | } 27 | 28 | export interface IBootParams { 29 | token: string; 30 | guildId: string; 31 | filesChannelName: string; 32 | metaChannelName: string; 33 | webdavPort: number; 34 | startWebdavServer: boolean; 35 | enableHttps: boolean; 36 | enableAuth: boolean; 37 | users: string; 38 | enableEncrypt: boolean; 39 | encryptPassword: string; 40 | saveTimeout: number; 41 | saveToDisk: boolean; 42 | } 43 | 44 | export interface IBootParamsParsed extends IBootParams { 45 | usersParsed: IUserRecord[]; 46 | } 47 | 48 | /** 49 | * Checks if the params are correct and in correct format. Function mutates the params object. Throws error if the params are not correct. 50 | * @param params - params to check 51 | * @returns - parsed params. 52 | */ 53 | function bootPrecheck(params: IBootParams): IBootParamsParsed { 54 | 55 | if (params.enableEncrypt) { 56 | // TODO: add password check for downloading encrypted files in not encrypted server. 57 | if (!params.encryptPassword) { 58 | throw new Error("Please set the ENCRYPT_PASSWORD to your encryption password."); 59 | } 60 | 61 | if (params.encryptPassword.length <= 0 && params.encryptPassword.length > 32) { 62 | throw new Error("ENCRYPT_PASSWORD env variable is not in correct format. Please set it to a password between 1 and 32 characters. Current length: " + params.encryptPassword.length); 63 | } 64 | 65 | params.encryptPassword = ensureStringLength(params.encryptPassword, 32); 66 | } 67 | 68 | // regex: key:value,key:value,... 69 | if (params.enableAuth && !(/^(?:\w+:\w+,)*\w+:\w+$/i).test(params.users)) { 70 | throw new Error("USERS env variable is not in correct format. Please use format username1:password1,username2:password2"); 71 | } 72 | 73 | const usersParsed: IUserRecord[] = params.users.split(",").map((user) => { 74 | const [username, password] = user.split(":"); 75 | return { username, password }; 76 | }); 77 | 78 | if (params.enableAuth && usersParsed.length == 0) { 79 | throw new Error("USERS env variable is empty. Please set at least one user."); 80 | } 81 | 82 | if (params.saveTimeout < 1) { 83 | throw new Error("SAVE_TIMEOUT env variable is set to < 1ms. Please set it to at least 1ms."); 84 | } 85 | 86 | if (params.metaChannelName.toLowerCase() === params.filesChannelName.toLowerCase()) { 87 | throw new Error("META_CHANNEL and FILES_CHANNEL env variables are set to the same value. Please set them to different values."); 88 | } 89 | 90 | return { 91 | ...params, 92 | usersParsed, 93 | }; 94 | } 95 | 96 | 97 | export async function boot(data: IBootParams): Promise { 98 | console.log(`NodeJS version: ${process.version}`); 99 | console.log("Starting DICloud..."); 100 | const params = bootPrecheck(data); 101 | const app = new DICloudApp({ 102 | intents: [ 103 | GatewayIntentBits.MessageContent, 104 | ], 105 | filesChannelName: params.filesChannelName, 106 | metaChannelName: params.metaChannelName, 107 | 108 | shouldEncrypt: params.enableEncrypt, 109 | encryptPassword: params.encryptPassword, 110 | 111 | saveTimeout: params.saveTimeout, 112 | saveToDisk: params.saveToDisk, 113 | }, params.guildId); 114 | 115 | Log.info("Logging in..."); 116 | await app.login(params.token); 117 | await app.init(); 118 | 119 | if (params.startWebdavServer) { 120 | const web = express(); 121 | 122 | const serverLaunchOptions: ServerOptions = { 123 | port: params.webdavPort, 124 | rootFileSystem: new DiscordWebdavFilesystemHandler(app), 125 | } 126 | 127 | if (params.enableHttps) { 128 | console.log("Detected ENABLE_HTTPS env variable. Starting webdav server with https enabled."); 129 | 130 | // generate self-signed certificate: openssl req -x509 -newkey rsa:4096 -keyout privkey.pem -out cert.pem -days 365 -nodes 131 | checkIfFileExists(root.resolve("/certs/privkey.pem"), false, "Please set ssl ./certs/privKey.pem or generate a self-signed certificate"); 132 | checkIfFileExists(root.resolve("/certs/cert.pem"), false, "Please set ssl ./certs/cert.pem or generate a self-signed certificate"); 133 | checkIfFileExists(root.resolve("/certs/chain.pem"), true, "If you have chain file, please set ssl ./certs/chain.pem or generate a self-signed certificate"); 134 | 135 | serverLaunchOptions.https = { 136 | key: readFileSyncOrUndefined(root.resolve("/certs/privkey.pem")), 137 | cert: readFileSyncOrUndefined(root.resolve("/certs/cert.pem")), 138 | ca: readFileSyncOrUndefined(root.resolve("/certs/chain.pem")), 139 | } 140 | } 141 | 142 | 143 | if (params.enableAuth) { 144 | if (params.users.length === 0) { 145 | console.log("Please set the USERS to your users in format username:password,username:password or add at least one user."); 146 | console.log("Adding default user: admin:admin"); 147 | params.usersParsed.push({ username: "admin", password: "admin" }); 148 | } 149 | 150 | console.log("Detected AUTH env variable. Starting webdav server with auth enabled."); 151 | serverLaunchOptions.users = params.usersParsed; 152 | 153 | web.use((req, res, next) => { 154 | if (!req.headers.authorization) { 155 | res.setHeader("WWW-Authenticate", 'Basic realm="DICloud Server"'); 156 | res.status(401).end(); 157 | return; 158 | } 159 | 160 | const auth = req.headers.authorization.split(" ")[1]; 161 | const [username, password] = Buffer.from(auth, "base64").toString().split(":"); 162 | 163 | const user = params.usersParsed.find((user) => user.username === username && user.password === password); 164 | if (!user) { 165 | res.setHeader("WWW-Authenticate", 'Basic realm="DICloud Server"'); 166 | res.status(401).end(); 167 | return; 168 | } 169 | 170 | next(); 171 | }); 172 | } 173 | 174 | Log.info("Starting webdav server..."); 175 | const webdavServer = WebdavServer.createServer(serverLaunchOptions, app); 176 | 177 | 178 | const relativePath = "dav"; 179 | web.use(express.json()); 180 | web.use(express.urlencoded({ extended: true })); 181 | web.use(webdav.extensions.express("/" + relativePath, webdavServer)); 182 | 183 | // zip folder download, have to be first, because of the regex and priority of the routes. 184 | web.get(/.*\.zip$/, async (req, res) => { 185 | const path = req.path.replace(".zip", ""); 186 | Log.info("[Zip] Requested path:", path); 187 | 188 | if(!app.getFs().existsSync(path)) { 189 | res.status(404).send("Not found"); 190 | return; 191 | } 192 | 193 | const zip = archiver("zip"); 194 | 195 | const files = app.getFs().getFilesWithPathRecursive(path); 196 | for (let [filePath, file] of Object.entries(files)) { 197 | // replace file path with relative path, so current relative path is root of the zip 198 | filePath = filePath.replace(path, ""); 199 | zip.append(await app.getProvider().createReadStream(file), { name: filePath }); 200 | } 201 | 202 | zip.finalize(); 203 | res.setHeader("Content-Type", "application/zip"); 204 | zip.pipe(res); 205 | 206 | }); 207 | 208 | 209 | // web file listings 210 | web.get("/*", async (req, res) => { 211 | const path = req.path; 212 | Log.info("Requested path:", path); 213 | 214 | if (!app.getFs().existsSync(path)) { 215 | res.status(404).send("Not found"); 216 | return; 217 | } 218 | const files = app.getFs().getFilesAndFolders(path); 219 | 220 | const parentPath = path.split('/').slice(0, -1).join('/') || '/'; 221 | 222 | const html = ` 223 | 224 | 225 | 226 | DICloud Files 227 | 302 | 303 | 304 | 310 |

DICloud Files

311 | 328 | 329 | `; 330 | res.send(html); 331 | }); 332 | 333 | await new Promise((resolve, reject) => { 334 | web.listen(params.webdavPort, () => { 335 | Log.info("WebDAV server started at port", params.webdavPort); 336 | resolve(); 337 | }).on('error', (err) => { 338 | reject(err); 339 | }); 340 | }); 341 | 342 | // debug logging for requests and responses 343 | webdavServer.beforeRequest((arg, next) => { 344 | Log.info("[S] IN [" + arg.request.socket.remoteAddress + "] > " + arg.request.method + ", " + arg.request.url); 345 | next(); 346 | }); 347 | 348 | webdavServer.afterRequest((arg, next) => { 349 | Log.info("[S] OUT [" + arg.request.socket.remoteAddress + "] >", "(" + arg.response.statusCode + ") " + arg.responseBody); 350 | next(); 351 | }); 352 | 353 | app.setWebdavServer(webdavServer); 354 | Log.info("Looks like everything is ready."); 355 | 356 | } 357 | 358 | return app 359 | } 360 | 361 | export async function envBoot() { 362 | const token = getEnv("TOKEN", "Please set the TOKEN to your bot token"); 363 | const guildId = getEnv("GUILD_ID", "Please set the GUILD_ID to your guild id"); 364 | const filesChannelName = getEnv("FILES_CHANNEL", "Please set the FILES_CHANNEL to your files channel name", "string", "files"); 365 | const metaChannelName = getEnv("META_CHANNEL", "Please set the META_CHANNEL to your meta channel name", "string", "meta"); 366 | 367 | const webdavPort = getEnv("PORT", "Please set the PORT to your webdav server port", "number", 3000) as number; 368 | 369 | const enableHttps = getEnv("ENABLE_HTTPS", "Please set the ENABLE_HTTPS to true or false to enable https", "boolean", false) as boolean; 370 | 371 | const enableAuth = getEnv("AUTH", "Please set the AUTH to true or false to enable auth", "boolean", false) as boolean; 372 | const users = getEnv("USERS", "Please set the USERS to your users in format username:password,username:password", "string", "") as string; 373 | 374 | const enableEncrypt = getEnv("ENCRYPT", "Please set the ENCRYPT to true or false to enable encryption", "boolean", false) as boolean; 375 | const encryptPassword = getEnv("ENCRYPT_PASS", "Please set the ENCRYPT_PASSWORD to your encryption password", "string", "") as string; 376 | 377 | const saveTimeout = getEnv("SAVE_TIMEOUT", "Please set the SAVE_TIMEOUT to your save timeout in ms", "number", 2000) as number; 378 | const saveToDisk = getEnv("SAVE_TO_DISK", "Please set the SAVE_TO_DISK to true or false to enable saving to disk", "boolean", false) as boolean; 379 | 380 | return await boot({ 381 | token, 382 | guildId, 383 | filesChannelName, 384 | metaChannelName, 385 | webdavPort, 386 | enableHttps, 387 | enableAuth, 388 | users: users, 389 | enableEncrypt, 390 | encryptPassword, 391 | saveTimeout, 392 | saveToDisk, 393 | startWebdavServer: true, 394 | }) 395 | 396 | } 397 | 398 | 399 | if (require.main === module) { 400 | process.on("uncaughtException", (err) => { 401 | console.log("Uncaught exception"); 402 | console.trace(err) 403 | // printAndExit("Uncaught exception, to prevent data loss, the app will be closed."); 404 | }); 405 | 406 | process.on("unhandledRejection", (reason, promise) => { 407 | console.trace(reason); 408 | }); 409 | 410 | envBoot(); 411 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | dicloud-storage: 3 | build: 4 | context: . 5 | dockerfile: Dockerfile 6 | ports: 7 | - "3000:3000" 8 | volumes: 9 | - ./certs:/app/certs 10 | environment: 11 | - NODE_ENV=production 12 | restart: unless-stopped 13 | networks: 14 | - dicloud-network 15 | 16 | networks: 17 | dicloud-network: 18 | driver: bridge -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import { boot, envBoot } from "./bootloader.js"; 2 | 3 | export default { 4 | boot, 5 | envBoot, 6 | } 7 | 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dicloud", 3 | "version": "3.0.0", 4 | "main": "out/boot.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "@noble/ciphers": "^1.0.0", 8 | "@ungap/structured-clone": "^1.2.0", 9 | "app-root-path": "^3.1.0", 10 | "archiver": "^7.0.1", 11 | "axios": "^1.3.6", 12 | "axios-retry": "^4.5.0", 13 | "discord.js": "^14.9.0", 14 | "dotenv": "^16.0.3", 15 | "express": "^4.21.2", 16 | "memfs": "^4.2.0", 17 | "mime-types": "^3.0.0", 18 | "object-hash": "^3.0.0", 19 | "webdav-server": "^2.6.2" 20 | }, 21 | "engines": { 22 | "node": ">=16", 23 | "yarn": ">=1.22.0" 24 | }, 25 | "scripts": { 26 | "start": "node --no-warnings out/bootloader.js", 27 | "boot": "tsc -p tsconfig.json && node --no-warnings out/bootloader.js", 28 | "build": "tsc -p tsconfig.json", 29 | "test": "yarn build && node --no-warnings out/tests/raw.spec.js", 30 | "del": "del /s /q out" 31 | }, 32 | "devDependencies": { 33 | "@types/archiver": "^6.0.3", 34 | "@types/express": "^5.0.0", 35 | "@types/mime-types": "^2.1.1", 36 | "@types/node": "^18.16.1", 37 | "@types/object-hash": "^3.0.3", 38 | "@types/ungap__structured-clone": "^0.3.0", 39 | "typescript": "^5.0.4", 40 | "uvu": "^0.5.6", 41 | "yarn-upgrade-all": "^0.7.2" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /scripts/webdav.reg: -------------------------------------------------------------------------------- 1 | Windows Registry Editor Version 5.00 2 | 3 | [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\WebClient\Parameters] 4 | "FileSizeLimitInBytes"=dword:ffffffff -------------------------------------------------------------------------------- /src/DICloudApp.ts: -------------------------------------------------------------------------------- 1 | import nodeFS from 'fs'; 2 | import os from 'os'; 3 | import DiscordFileProvider, { MAX_CHUNK_SIZE } from './provider/DiscordFileProvider.js'; 4 | import axios from './helper/AxiosInstance.js'; 5 | import { IFile, IFilesDesc } from './file/IFile.js'; 6 | import { ChannelType, Client, ClientOptions, FetchMessagesOptions, Guild, Message, TextChannel } from 'discord.js'; 7 | import VolumeEx from './file/VolumeEx.js'; 8 | import objectHash from "object-hash"; 9 | import { printAndExit } from './helper/utils.js'; 10 | import BaseProvider from './provider/BaseProvider.js'; 11 | import WebdavServer from './webdav/WebdavServer.js'; 12 | import { Readable, Writable } from 'stream'; 13 | import Log from './Log.js'; 14 | 15 | export interface DICloudAppOptions extends ClientOptions { 16 | metaChannelName: string; 17 | filesChannelName: string; 18 | 19 | shouldEncrypt: boolean; 20 | encryptPassword?: string; 21 | 22 | saveTimeout: number; 23 | saveToDisk: boolean; 24 | } 25 | 26 | /** 27 | * Main class of the DICloud. Most functions are designed to work with webdav. 28 | */ 29 | export default class DICloudApp { 30 | public static instance: DICloudApp; 31 | 32 | private guildId: string; 33 | private metaChannelName: string; 34 | private filesChannelId: string; 35 | private createChannels: Array; 36 | private provider: BaseProvider; 37 | 38 | private shouldEncrypt; 39 | private encryptPassword; 40 | 41 | 42 | private metadataMessage!: Message; 43 | private fs!: VolumeEx; 44 | 45 | private debounceTimeout: NodeJS.Timeout | undefined; 46 | private debounceTimeoutTime: number; 47 | 48 | private saveToDisk: boolean = false; 49 | 50 | private tickInterval: NodeJS.Timeout | undefined; 51 | private tickIntervalTime: number = 1000; 52 | 53 | private readonly medataInfoMessage: string = "DiscordFS Metadata ✔"; 54 | 55 | private guild!: Guild; 56 | private metaChannel!: TextChannel; 57 | private filesChannel!: TextChannel; 58 | 59 | 60 | private discordClient: Client 61 | private webdavServer?: WebdavServer; 62 | // private app = express(); 63 | 64 | 65 | constructor(options: DICloudAppOptions, guildId: string) { 66 | this.discordClient = new Client(options); 67 | if (DICloudApp.instance) { 68 | throw new Error("DICloud already running"); 69 | } 70 | DICloudApp.instance = this; 71 | 72 | this.createChannels = [ 73 | options.metaChannelName, 74 | options.filesChannelName 75 | ]; 76 | 77 | this.metaChannelName = options.metaChannelName; 78 | this.filesChannelId = options.filesChannelName; 79 | 80 | this.shouldEncrypt = options.shouldEncrypt; 81 | this.encryptPassword = options.encryptPassword ?? ""; 82 | 83 | 84 | this.debounceTimeoutTime = options.saveTimeout; 85 | this.saveToDisk = options.saveToDisk; 86 | 87 | 88 | this.guildId = guildId; 89 | this.provider = new DiscordFileProvider(this); 90 | } 91 | 92 | public shouldEncryptFiles(): boolean { 93 | return this.shouldEncrypt; 94 | } 95 | 96 | public getEncryptPassword(): string { 97 | return this.encryptPassword; 98 | } 99 | 100 | public getGuild(): Guild { 101 | return this.guild; 102 | }; 103 | 104 | public getMetadataChannel(): TextChannel { 105 | return this.metaChannel; 106 | } 107 | 108 | public getFilesChannel(): TextChannel { 109 | return this.filesChannel; 110 | } 111 | 112 | public async waitForReady(): Promise { 113 | return new Promise((resolve, reject) => { 114 | this.discordClient.once("ready", resolve as any); 115 | }); 116 | } 117 | 118 | public async login(token: string): Promise { 119 | await this.discordClient.login(token); 120 | } 121 | 122 | public async init() { 123 | Log.info("Initializing DICloud..."); 124 | await this.waitForReady(); 125 | Log.info("Client authenticated...") 126 | await this.preload(); 127 | await this.loadFiles(); 128 | Log.info("DICloud initialized, files loaded: ", Object.keys(this.fs.toJSON()).length); 129 | } 130 | 131 | /** 132 | * Should be called after the bot is ready. 133 | * Preloads required data and starts the tick interval. 134 | */ 135 | private async preload() { 136 | Log.info("Fetching guilds..."); 137 | await this.discordClient.guilds.fetch(); 138 | 139 | if (!this.discordClient.guilds.cache.has(this.guildId)) { 140 | printAndExit("Provided guild not found. Is the bot in the guild?"); 141 | } 142 | 143 | const guild = await this.discordClient.guilds.cache.get(this.guildId)?.fetch(); 144 | if (!guild) { 145 | printAndExit("Failed to fetch guild: " + this.guildId); 146 | } 147 | this.guild = guild!; 148 | Log.info("Guild found: " + this.guild.name); 149 | 150 | Log.info("Fetching channels..."); 151 | await this.guild.channels.fetch(); 152 | let channels = this.guild.channels.cache.filter(channel => channel.type == ChannelType.GuildText); 153 | 154 | let wasChannelCreated = false; // using for caching 155 | for (const channel of this.createChannels) { 156 | if (!channels.some(c => c.name == channel)) { 157 | Log.info("Creating channel: " + channel); 158 | await this.guild.channels.create({ 159 | name: channel, 160 | type: ChannelType.GuildText, 161 | }); 162 | wasChannelCreated = true; 163 | } 164 | } 165 | 166 | // Caching again, because we created new ones 167 | if(wasChannelCreated) { 168 | await this.guild.channels.fetch(); 169 | channels = this.guild.channels.cache.filter(channel => channel.type == ChannelType.GuildText); 170 | } 171 | 172 | this.metaChannel = channels.find(channel => channel.name == this.metaChannelName) as TextChannel 173 | this.filesChannel = channels.find(channel => channel.name == this.filesChannelId) as TextChannel; 174 | 175 | this.tickInterval = setInterval(() => { 176 | this.tick(); 177 | }, this.tickIntervalTime); 178 | 179 | } 180 | 181 | 182 | 183 | async getAllMessages(channelId: string): Promise { 184 | const channel = await this.discordClient.channels.fetch(channelId) as TextChannel; 185 | let messages: Message[] = []; 186 | let last: string | undefined; 187 | 188 | while (true) { 189 | const options: FetchMessagesOptions = { limit: 100 }; 190 | if (last) { 191 | options.before = last; 192 | } 193 | 194 | const channelMessages = [... (await channel.messages.fetch(options)).values()]; 195 | 196 | messages = messages.concat(channelMessages); 197 | Log.info("[getAllMessages] got block of " + channelMessages.length + " messages") 198 | if (channelMessages.length < 100) { 199 | break; 200 | } 201 | 202 | last = channelMessages.pop()!.id; 203 | } 204 | 205 | return messages; 206 | } 207 | 208 | 209 | private async loadFiles() { 210 | const messages = await this.getAllMessages(this.getMetadataChannel().id); 211 | let message; // meta message 212 | 213 | // check if there is a message with the metadata info. If not, create one. 214 | if (messages.length == 0) { // no messages 215 | message = await this.getMetadataChannel().send({ 216 | files: [{ 217 | attachment: Buffer.from("{}"), // empty json file 218 | name: "discordfs.json" 219 | }], 220 | content: this.medataInfoMessage 221 | }) 222 | } else if (messages.length == 1) { // one message 223 | message = messages[0]; 224 | } else { 225 | throw new Error("Invalid amount of messages in metadata channel, there should only be one message. Maybe wrong channel is provided?"); 226 | } 227 | 228 | if (message.attachments.size != 1) { 229 | throw new Error("Invalid amount of attachments in metadata message"); 230 | } 231 | 232 | const attachment = message.attachments.first()!; 233 | if (attachment.name != "discordfs.json") { 234 | throw new Error("Invalid attachment name in metadata message, expected discordfs.json, got: " + attachment.name); 235 | } 236 | 237 | const file = await axios.get(attachment.url, { responseType: "arraybuffer" }); 238 | try { 239 | const data = JSON.parse(file.data.toString()) as IFilesDesc; 240 | 241 | this.fs = VolumeEx.fromJSON(data as any); 242 | this.metadataMessage = message; 243 | 244 | } catch (e) { 245 | Log.error(e); 246 | printAndExit("Failed to parse JSON file. Is the file corrupted?"); 247 | } 248 | 249 | 250 | } 251 | 252 | public saveFiles(saveToDriveOnly: boolean = false, driveSaveForce: boolean = false): Promise { 253 | Log.info("Saving files..."); 254 | return new Promise((resolve, reject) => { 255 | if (this.saveToDisk || driveSaveForce) { 256 | this.saveToDrive(); 257 | 258 | if (saveToDriveOnly) { 259 | resolve(); 260 | return; 261 | } 262 | } 263 | 264 | const json = this.fs.toJSON() 265 | const file = JSON.stringify(json); 266 | 267 | this.metadataMessage.edit({ 268 | files: [{ 269 | name: "discordfs.json", 270 | attachment: Buffer.from(file) 271 | }], 272 | content: this.medataInfoMessage 273 | + '\n\n' + 'Last saved: ' + new Date().toLocaleString() 274 | + '\n' + 'Database Size: ' + file.length + ' bytes (' + Math.floor(file.length / MAX_CHUNK_SIZE * 100) + ' %)' 275 | + '\n' + 'Files: ' + Object.keys(json).length + ' files' 276 | + '\n' + 'Total Size: ' + (Math.floor(this.fs.getTreeSizeRecursive("/") / 1000 / 1000)) + ' MB' 277 | + '\n' + 'Hash: (' + objectHash(json) + ')' 278 | }) 279 | .then(() => resolve()) 280 | .catch((err) => { 281 | Log.info("Failed to save metadata message: " + err); 282 | this.saveToDrive(); 283 | reject(err); 284 | }); 285 | }); 286 | } 287 | 288 | private saveToDrive() { 289 | Log.info("Saving files to disk... ( " + os.tmpdir() + "/discordfs.json )"); 290 | nodeFS.writeFileSync(os.tmpdir() + "/discordfs.json", JSON.stringify(this.fs.toJSON())); 291 | } 292 | 293 | 294 | /** 295 | * Method that indicates that files were changed and should be saved to the provider. 296 | */ 297 | public markForUpload() { 298 | if (this.debounceTimeout) { 299 | clearTimeout(this.debounceTimeout); 300 | this.debounceTimeout = undefined; 301 | } 302 | 303 | this.debounceTimeout = setTimeout(() => { 304 | this.debounceTimeout = undefined; 305 | this.saveFiles(); 306 | }, this.debounceTimeoutTime); 307 | } 308 | 309 | /** 310 | * Handler for the tick interval. This will delete messages from the deletion queue. 311 | * Queue is used to prevent ratelimiting and blocking the bot from doing other things. 312 | */ 313 | private async tick() { 314 | await this.provider.processDeletionQueue(); 315 | } 316 | 317 | public getProvider(): BaseProvider { 318 | return this.provider; 319 | } 320 | 321 | public getFs() { 322 | return this.fs; 323 | } 324 | 325 | public setWebdavServer(server: WebdavServer) { 326 | this.webdavServer = server; 327 | } 328 | 329 | public getWebdavServer() { 330 | return this.webdavServer; 331 | } 332 | 333 | public getDiscordClient() { 334 | return this.discordClient; 335 | } 336 | 337 | public createWriteStream(file: IFile): Promise { 338 | return this.provider.createRawWriteStream(file); 339 | } 340 | 341 | public createReadStream(file: IFile): Promise { 342 | return this.provider.createReadStream(file); 343 | } 344 | 345 | 346 | public async uploadFile(Buffer: Buffer, name: string): Promise { 347 | return this.provider.uploadFile(Buffer, name); 348 | } 349 | 350 | public async downloadFile(file: IFile): Promise { 351 | return this.provider.downloadFile(file); 352 | } 353 | 354 | public async shutdown(saveToDfive: boolean = false): Promise { 355 | if (this.webdavServer){ 356 | await this.webdavServer.stopAsync(); 357 | } 358 | clearInterval(this.tickInterval); 359 | clearInterval(this.debounceTimeout); 360 | 361 | await this.saveFiles(false, saveToDfive); 362 | await this.discordClient.destroy(); 363 | } 364 | 365 | } 366 | 367 | -------------------------------------------------------------------------------- /src/HttpStreamPool.ts: -------------------------------------------------------------------------------- 1 | import Log from "./Log.js"; 2 | import structuredClone from "@ungap/structured-clone"; 3 | import client from "./helper/AxiosInstance.js"; 4 | 5 | import { AxiosError } from "axios"; 6 | import { IChunkInfo, IFile } from "./file/IFile.js"; 7 | import { Readable, PassThrough } from "stream"; 8 | // import { patchEmitter } from "./helper/EventPatcher.js"; 9 | 10 | 11 | export default class HttpStreamPool { 12 | private chunks: IChunkInfo[]; 13 | private totalSize: number; 14 | private gotSize = 0; 15 | private currentUrlIndex = 0; 16 | private userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36" 17 | private downloadingFileName: string; 18 | private isCancelled = false; 19 | 20 | constructor(file: IFile, userAgent?: string) { 21 | file = structuredClone(file); 22 | 23 | this.chunks = file.chunks; 24 | this.totalSize = file.size; 25 | this.downloadingFileName = file.name; 26 | 27 | if (userAgent) { 28 | this.userAgent = userAgent; 29 | } 30 | } 31 | 32 | public async getDownloadStream(resolver: (id: string) => Promise): Promise { 33 | if (this.chunks.length == 0) { 34 | console.warn("[HttpStreamPool] No urls to download, returning empty stream"); 35 | return Readable.from([]); 36 | } 37 | 38 | const stream = new PassThrough(); 39 | const next = async () => { 40 | if (this.isCancelled) { // extra check against race conditions, since we are using async functions. 41 | Log.info("[HttpStreamPool] Downloading cancelled: " + this.downloadingFileName); 42 | this.cleanupStream(stream); 43 | return; 44 | } 45 | 46 | if (this.currentUrlIndex >= this.chunks.length) { 47 | Log.info("[HttpStreamPool] Downloading finished: " + this.downloadingFileName); 48 | // patchEmitter(stream, "HttpStreamPool", [/progress/]); 49 | this.cleanupStream(stream); 50 | return; 51 | } 52 | 53 | if (stream.closed || stream.destroyed) { 54 | Log.info("[HttpStreamPool] Stream closed; aborting"); 55 | this.cleanupStream(stream); 56 | return; 57 | } 58 | 59 | try { 60 | if (this.isCancelled) { 61 | Log.info("[HttpStreamPool] Download cancelled before starting chunk"); 62 | this.cleanupStream(stream); 63 | return; 64 | } 65 | 66 | Log.info("Resolving attachment URL for message: " + this.chunks[this.currentUrlIndex].id); 67 | const url = await resolver(this.chunks[this.currentUrlIndex].id); 68 | 69 | const res = await client.get(url, { 70 | responseType: "stream", 71 | headers: { 72 | "User-Agent": this.userAgent, 73 | }, 74 | timeout: 10000, 75 | }); 76 | 77 | res.data.on("data", (chunk: Buffer) => { 78 | this.gotSize += chunk.length; 79 | const progress = this.totalSize > 0 ? this.gotSize / this.totalSize : 0; 80 | stream.emit("progress", this.gotSize, this.totalSize, progress); 81 | }); 82 | 83 | res.data.on("end", () => { 84 | this.currentUrlIndex++; 85 | next(); 86 | }); 87 | 88 | res.data.on("error", (err: Error) => { 89 | this.handleError(err, stream); 90 | }); 91 | 92 | res.data.pipe(stream, { end: false }); 93 | } catch (err) { 94 | this.handleError(err, stream); 95 | } 96 | }; 97 | 98 | next(); 99 | return stream; 100 | } 101 | 102 | private handleError(err: unknown, stream: PassThrough) { 103 | if (err instanceof AxiosError && err.code === 'ECONNABORTED') { 104 | console.error("[HttpStreamPool] Request timeout:", err.message); 105 | stream.emit("error", new Error("Request timeout")); 106 | } else { 107 | console.error("[HttpStreamPool] Error:", err); 108 | stream.emit("error", err instanceof Error ? err : new Error("Unknown error occurred")); 109 | } 110 | this.cleanupStream(stream); 111 | } 112 | 113 | private cleanupStream(stream: PassThrough) { 114 | stream.end(); 115 | this.cancelDownload(); 116 | } 117 | 118 | public cancelDownload() { 119 | this.isCancelled = true; 120 | } 121 | } -------------------------------------------------------------------------------- /src/Log.ts: -------------------------------------------------------------------------------- 1 | export default class Log { 2 | public static info(message: any, ...args: any[]) { 3 | console.log(message, ...args) 4 | } 5 | 6 | static error(message: any, ...args: any[]) { 7 | console.error(message, ...args) 8 | } 9 | } -------------------------------------------------------------------------------- /src/file/IFile.ts: -------------------------------------------------------------------------------- 1 | import { randomBytes } from '@noble/ciphers/webcrypto'; 2 | 3 | export interface IFile { 4 | name: string; // name is used only for raw provider, not for webdav. Webdav uses paths. 5 | size: number; 6 | chunks: IChunkInfo[] 7 | created: Date; 8 | modified: Date; 9 | iv: Uint8Array 10 | // uploaded: boolean; 11 | encrypted: boolean; 12 | } 13 | 14 | export interface IChunkInfo { 15 | id: string; // discord (or any other provider) message id 16 | size: number; 17 | // url: string; 18 | } 19 | 20 | 21 | export type IFilesDesc = Record; 22 | 23 | 24 | /** 25 | * Returns file struct, no remote operations are done. 26 | */ 27 | export function createVFile(name: string, size: number = 0, encrypted: boolean): IFile { 28 | return { 29 | name, 30 | size, 31 | chunks: [], 32 | created: new Date(), 33 | modified: new Date(), 34 | encrypted, 35 | iv: encrypted ? randomBytes(16) : new Uint8Array(0) 36 | }; 37 | } -------------------------------------------------------------------------------- /src/file/VolumeEx.ts: -------------------------------------------------------------------------------- 1 | import { DirectoryJSON, Volume } from "memfs/lib/volume.js"; 2 | import { IFile } from "./IFile.js"; 3 | import Dirent from "memfs/lib/Dirent.js"; 4 | 5 | export interface IEntry { 6 | file: boolean, 7 | name: string, 8 | } 9 | 10 | export default class VolumeEx extends Volume { 11 | 12 | public static fromJSON(json: DirectoryJSON, cwd?: string | undefined): VolumeEx { 13 | const vol = new VolumeEx(cwd); 14 | vol.fromJSON(json); 15 | return vol; 16 | } 17 | 18 | public pathExists(path: string): boolean { 19 | try { 20 | this.statSync(path); 21 | return true; 22 | } catch (e) { 23 | return false; 24 | } 25 | } 26 | 27 | public getFile(path: string): IFile { 28 | return JSON.parse(this.readFileSync(path).toString(), (k, v) => { 29 | if (k === "created" || k === "modified") { 30 | return new Date(v); 31 | } 32 | if (k === "iv") { 33 | return new Uint8Array(Object.values(v)); 34 | } 35 | 36 | return v; 37 | }) as IFile; 38 | } 39 | 40 | public setFile(path: string, file: IFile) { 41 | this.writeFileSync(path, JSON.stringify(file)); 42 | } 43 | 44 | private getFilesPathsRecursive(initial: string, paths: string[] = []) { 45 | const entries = this.readdirSync(initial, { withFileTypes: true }); 46 | for (const entry of entries) { 47 | const path = initial === '/' ? '/' + (entry as Dirent).name : initial + '/' + (entry as Dirent).name; 48 | if ((entry as Dirent).isDirectory()) { 49 | this.getFilesPathsRecursive(path, paths); 50 | } else { 51 | paths.push(path); 52 | } 53 | } 54 | return paths; 55 | } 56 | 57 | public getFilesRecursive(path: string): IFile[] { 58 | return this.getFilesPathsRecursive(path).map(p => this.getFile(p)); 59 | } 60 | 61 | public getFilesWithPathRecursive(path: string): Record { 62 | return this.getFilesPathsRecursive(path).reduce((acc, path) => { 63 | acc[path] = this.getFile(path); 64 | return acc; 65 | }, {} as Record); 66 | } 67 | 68 | public getPathsRecursive(path: string): string[] { 69 | return this.getFilesPathsRecursive(path); 70 | } 71 | 72 | public getTreeSizeRecursive(path: string): number { 73 | return this.getFilesRecursive(path).reduce((acc, file) => acc + file.size, 0); 74 | } 75 | 76 | // todo: reorganize this 77 | public getFilesAndFolders(path: string): IEntry[] { 78 | return this.readdirSync(path, { withFileTypes: true }).map((entry) => { 79 | return { 80 | file: (entry as Dirent).isFile(), 81 | name: (entry as Dirent).name.toString(), 82 | } 83 | }); 84 | } 85 | 86 | 87 | 88 | 89 | 90 | } -------------------------------------------------------------------------------- /src/helper/AxiosInstance.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import axiosRetry from "axios-retry"; 3 | 4 | const client = axios.create(); 5 | 6 | axiosRetry(client, { retries: 3 }); 7 | 8 | export default client; -------------------------------------------------------------------------------- /src/helper/EventPatcher.ts: -------------------------------------------------------------------------------- 1 | import Log from "../Log"; 2 | 3 | /** 4 | * Debugging helper to log all events emitted by the given emitter without having to manually add a listener for each event. 5 | * @param emitter The emitter to patch. 6 | */ 7 | export const patchEmitter = (emitter: any, label: string, ignoredEvents: RegExp[] = []) => { 8 | const oldEmit = emitter.emit; 9 | 10 | emitter.emit = function () { 11 | let emitArgs = arguments; 12 | let eventName = emitArgs[0]; 13 | 14 | 15 | if (!ignoredEvents.some((re) => re.test(eventName))) { 16 | Log.info("EmitterPatcher["+label+"] event: " + eventName, "\n", Array.from(emitArgs).splice(0, 2)); 17 | } 18 | 19 | oldEmit.apply(emitter, arguments as any); 20 | } as any; 21 | } 22 | -------------------------------------------------------------------------------- /src/helper/MutableBuffer.ts: -------------------------------------------------------------------------------- 1 | // thanks to https://github.com/taoyuan/mutable-buffer/ 2 | 3 | const DEFAULT_INITIAL_SIZE = 1024; 4 | const DEFAULT_BLOCK_SIZE = 1024; 5 | 6 | type WriteData = BaseMutableBuffer | string | Buffer | ArrayLike | ArrayBuffer | SharedArrayBuffer; 7 | 8 | 9 | export class BaseMutableBuffer { 10 | static readonly target: string; 11 | 12 | protected _initialSize: number; 13 | protected _blockSize: number; 14 | protected _size: number; 15 | protected _buffer: Buffer; 16 | 17 | 18 | constructor(size?: number, blockSize?: number) { 19 | this._initialSize = size ?? DEFAULT_INITIAL_SIZE; 20 | this._blockSize = blockSize ?? DEFAULT_BLOCK_SIZE; 21 | 22 | this._buffer = this.Buffer.allocUnsafe(this._initialSize); 23 | this._size = 0; 24 | } 25 | 26 | get size() { 27 | return this._size; 28 | } 29 | 30 | 31 | get buffer(): Buffer { 32 | return this._buffer; 33 | } 34 | 35 | get Buffer(): typeof Buffer { 36 | return (this.constructor).Buffer; 37 | } 38 | 39 | static create(size?: number, blockSize?: number) { 40 | return new this(size, blockSize); 41 | } 42 | 43 | //resize internal buffer if not enough size left 44 | _ensure(size: number) { 45 | const remaining = this._buffer.length - this._size; 46 | if (remaining < size) { 47 | const factor = Math.ceil((size - remaining) / this._blockSize); 48 | 49 | const prev = this._buffer; 50 | this._buffer = this.Buffer.allocUnsafe(prev.length + this._blockSize * factor); 51 | prev.copy(this._buffer); 52 | } 53 | } 54 | 55 | capacity() { 56 | return this._buffer.length; 57 | } 58 | 59 | cloneNativeBuffer() { 60 | return this._buffer.subarray(0, this._size); 61 | } 62 | 63 | clear() { 64 | this._size = 0; 65 | } 66 | 67 | destroy() { 68 | this._buffer = this.Buffer.allocUnsafe(0); 69 | this._size = 0; 70 | } 71 | 72 | // recreate(size?: number, blockSize?: number) { 73 | // this._initialSize = size ?? DEFAULT_INITIAL_SIZE; 74 | // this._blockSize = blockSize ?? DEFAULT_BLOCK_SIZE; 75 | 76 | // this._buffer = this.Buffer.allocUnsafe(this._initialSize); 77 | // this._size = 0; 78 | // } 79 | 80 | 81 | 82 | /** 83 | * 84 | * @param targetOrCreate The target buffer or creating or slice buffer. 85 | * 1. Buffer: The target buffer to render; 86 | * 2. true: Create new buffer and copy all cached data to it; 87 | * 3 false: Slice the cached data from internal buffer, The result cloud be be changed if current MutableBuffer has been reused. 88 | */ 89 | render(targetOrCreate?: Buffer | boolean): Buffer { 90 | if (targetOrCreate) { 91 | const answer = isBuffer(targetOrCreate) ? targetOrCreate : this.Buffer.allocUnsafe(this.size); 92 | this._buffer.copy(answer, 0, 0, this._size); 93 | return answer; 94 | } 95 | return this._buffer.subarray(0, this._size); 96 | } 97 | 98 | flush(targetOrCreate?: Buffer | boolean) { 99 | const result = this.render(targetOrCreate); 100 | this.clear(); 101 | return result; 102 | } 103 | 104 | flushAndDestory(): Buffer { 105 | const result = this.flush(true); 106 | this.destroy(); 107 | return result; 108 | } 109 | 110 | 111 | write(source: WriteData, encoding?: BufferEncoding): number; 112 | write(source: WriteData, ...args: any[]): number; 113 | write(source: WriteData, ...args: any[]): number { 114 | if (isBuffer(source)) { 115 | this._ensure(source.length); 116 | source.copy(this._buffer, this._size); 117 | this._size += source.length; 118 | } else if (Array.isArray(source)) { 119 | this._ensure(source.length); 120 | for (let i = 0; i < source.length; i++) { 121 | this._buffer[this._size + i] = source[i]; 122 | } 123 | this._size += source.length; 124 | } else if (isMutableBuffer(source)) { 125 | this._ensure(source.size); 126 | source.buffer.copy(this._buffer, this._size); 127 | this._size += source.size; 128 | } else { 129 | const last = args.length > 0 ? args[args.length - 1] : undefined; 130 | const encoding = typeof last === 'string' ? (last as BufferEncoding) : undefined; 131 | source = source + ''; 132 | const len = this.Buffer.byteLength(source, encoding); 133 | this._ensure(len); 134 | this._buffer.write(source, this._size, len, encoding); 135 | this._size += len; 136 | } 137 | return this.size; 138 | } 139 | 140 | writeCString(data?: string | Buffer, encoding?: BufferEncoding) { 141 | //just write a 0 for empty or null strings 142 | if (!data) { 143 | this._ensure(1); 144 | } else if (isBuffer(data)) { 145 | this._ensure(data.length); 146 | data.copy(this._buffer, this._size); 147 | this._size += data.length; 148 | } else { 149 | const len = this.Buffer.byteLength(data, encoding); 150 | this._ensure(len + 1); //+1 for null terminator 151 | this._buffer.write(data, this._size, len, encoding); 152 | this._size += len; 153 | } 154 | 155 | this._buffer[this._size++] = 0; // null terminator 156 | return this.size; 157 | } 158 | 159 | writeChar(c: string) { 160 | this._ensure(1); 161 | this._buffer.write(c, this._size, 1); 162 | this._size++; 163 | return this.size; 164 | } 165 | 166 | writeUIntLE(value: number, byteLength: number) { 167 | this._ensure(byteLength >>> 0); 168 | this._size = this._buffer.writeUIntLE(value, this._size, byteLength); 169 | return this.size; 170 | } 171 | 172 | writeUIntBE(value: number, byteLength: number) { 173 | this._ensure(byteLength >>> 0); 174 | this._size = this._buffer.writeUIntBE(value, this._size, byteLength); 175 | return this.size; 176 | } 177 | 178 | writeUInt8(value: number) { 179 | this._ensure(1); 180 | this._size = this._buffer.writeUInt8(value, this._size); 181 | return this.size; 182 | } 183 | 184 | writeUInt16LE(value: number) { 185 | this._ensure(2); 186 | this._size = this._buffer.writeUInt16LE(value, this._size); 187 | return this.size; 188 | } 189 | 190 | writeUInt16BE(value: number) { 191 | this._ensure(2); 192 | this._size = this._buffer.writeUInt16BE(value, this._size); 193 | return this.size; 194 | } 195 | 196 | writeUInt32LE(value: number) { 197 | this._ensure(4); 198 | this._size = this._buffer.writeUInt32LE(value, this._size); 199 | return this.size; 200 | } 201 | 202 | writeUInt32BE(value: number) { 203 | this._ensure(4); 204 | this._size = this._buffer.writeUInt32BE(value, this._size); 205 | return this.size; 206 | } 207 | 208 | writeIntLE(value: number, byteLength: number) { 209 | this._ensure(byteLength >>> 0); 210 | this._size = this._buffer.writeIntLE(value, this._size, byteLength); 211 | return this.size; 212 | } 213 | 214 | writeIntBE(value: number, byteLength: number) { 215 | this._ensure(byteLength >>> 0); 216 | this._size = this._buffer.writeIntBE(value, this._size, byteLength); 217 | return this.size; 218 | } 219 | 220 | writeInt8(value: number) { 221 | this._ensure(1); 222 | this._size = this._buffer.writeInt8(value, this._size); 223 | return this.size; 224 | } 225 | 226 | writeInt16LE(value: number) { 227 | this._ensure(2); 228 | this._size = this._buffer.writeInt16LE(value, this._size); 229 | return this.size; 230 | } 231 | 232 | writeInt16BE(value: number) { 233 | this._ensure(2); 234 | this._size = this._buffer.writeInt16BE(value, this._size); 235 | return this.size; 236 | } 237 | 238 | writeInt32LE(value: number) { 239 | this._ensure(4); 240 | this._size = this._buffer.writeInt32LE(value, this._size); 241 | return this.size; 242 | } 243 | 244 | writeInt32BE(value: number) { 245 | this._ensure(4); 246 | this._size = this._buffer.writeInt32BE(value, this._size); 247 | return this.size; 248 | } 249 | 250 | writeFloatLE(value: number) { 251 | this._ensure(4); 252 | this._size = this._buffer.writeFloatLE(value, this._size); 253 | return this.size; 254 | } 255 | 256 | writeFloatBE(value: number) { 257 | this._ensure(4); 258 | this._size = this._buffer.writeFloatBE(value, this._size); 259 | return this.size; 260 | } 261 | 262 | writeDoubleLE(value: number) { 263 | this._ensure(8); 264 | this._size = this._buffer.writeDoubleLE(value, this._size); 265 | return this.size; 266 | } 267 | 268 | writeDoubleBE(value: number) { 269 | this._ensure(8); 270 | this._size = this._buffer.writeDoubleBE(value, this._size); 271 | return this.size; 272 | } 273 | 274 | 275 | trim() { 276 | if (this.size <= 0) { 277 | return this.size; 278 | } 279 | 280 | let begin = 0; 281 | let end = 0; 282 | 283 | for (let i = 0; i < this.size; i++) { 284 | if (this._buffer[i]) { 285 | begin = i; 286 | break; 287 | } 288 | } 289 | 290 | for (let i = this.size; i > 0; i--) { 291 | if (this._buffer[i - 1]) { 292 | end = i; 293 | break; 294 | } 295 | } 296 | 297 | if (begin === 0 && end === this.size) { 298 | return this.size; 299 | } 300 | 301 | this._buffer = this._buffer.subarray(begin, end); 302 | this._size = end - begin; 303 | return this.size; 304 | } 305 | 306 | trimLeft() { 307 | if (this.size <= 0 || this._buffer[0]) { 308 | return this.size; 309 | } 310 | 311 | for (let i = 0; i < this.size; i++) { 312 | if (this._buffer[i]) { 313 | this._buffer = this._buffer.subarray(i); 314 | this._size = this.size - i; 315 | return this.size; 316 | } 317 | } 318 | if (this.size > 0) { 319 | this._size = 0; 320 | } 321 | return this.size; 322 | } 323 | 324 | trimRight() { 325 | if (this.size <= 0 || this._buffer[this.size - 1]) { 326 | return this.size; 327 | } 328 | 329 | for (let i = this.size; i > 0; i--) { 330 | if (this._buffer[i - 1]) { 331 | this._buffer = this._buffer.subarray(0, i); 332 | this._size = i; 333 | return this.size; 334 | } 335 | } 336 | 337 | if (this.size > 0) { 338 | this._size = 0; 339 | } 340 | return this.size; 341 | } 342 | } 343 | 344 | export default class MutableBuffer extends BaseMutableBuffer { 345 | static readonly target = 'node'; 346 | static Buffer = Buffer; 347 | } 348 | 349 | function isBuffer(obj: any): obj is Buffer { 350 | return typeof obj?.constructor.isBuffer === 'function' && obj.constructor.isBuffer(obj); 351 | } 352 | 353 | function isMutableBuffer(obj: any): obj is BaseMutableBuffer { 354 | return obj?.buffer && obj.size && typeof obj.render === 'function'; 355 | } -------------------------------------------------------------------------------- /src/helper/utils.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | 3 | export function truncate(str: string, n: number, includeDots: boolean = false) { 4 | return ((str.length > n) ? str.substr(0, n - 1) : str) + (includeDots && str.length > n ? '...' : ''); 5 | } 6 | 7 | export function printAndExit(message: string, exitCode: number = 1) { 8 | console.log(message); 9 | process.exit(exitCode); 10 | } 11 | 12 | export function getEnv(name: string, assertString: string, type: "string" | "number" | "boolean" = "string", defaultValue?: any): any { 13 | const value = process.env[name]!; 14 | if (!value) { 15 | if (defaultValue !== undefined) { 16 | console.log("Env variable " + name + " is not set" + (assertString.length > 0 ? ": " + assertString : "") + ". Using default value: " + (defaultValue === "" ? "N/A" : defaultValue)); 17 | return defaultValue; 18 | } 19 | printAndExit("Required env variable " + name + " is not set" + (assertString.length > 0 ? ": " + assertString : "") + ". Please set it in .env file or in your system environment variables."); 20 | }; 21 | 22 | const valueLower = value.toLowerCase(); 23 | 24 | if (type == "boolean") { 25 | if (valueLower === "true") { 26 | return true; 27 | } else if (valueLower === "false") { 28 | return false; 29 | } else { 30 | printAndExit("Env variable " + name + " is not set to true or false" + (assertString.length > 0 ? ": " + assertString : "") + ". Please set it in .env file or in your system environment variables."); 31 | } 32 | } 33 | 34 | if (type == "number") { 35 | const number = parseInt(value!); 36 | if (isNaN(number)) { 37 | printAndExit("Env variable " + name + " is not set to number" + (assertString.length > 0 ? ": " + assertString : "") + ". Please set it in .env file or in your system environment variables."); 38 | } 39 | return number; 40 | } 41 | 42 | if (type == "string" && value.length == 0) { 43 | printAndExit("Env variable " + name + " is empty" + (assertString.length > 0 ? ": " + assertString : "") + ". Please set it in .env file or in your system environment variables."); 44 | } 45 | 46 | return value; 47 | } 48 | 49 | export function readFileSyncOrUndefined(path: string): Buffer | undefined { 50 | try { 51 | return fs.readFileSync(path); 52 | } catch (error) { 53 | console.warn("File " + path + " is not found. Skipping."); 54 | return undefined; 55 | } 56 | } 57 | 58 | export function checkIfFileExists(path: string, soft: boolean, assertString: string = ""): boolean { 59 | try { 60 | if (!fs.statSync(path).isFile()) { 61 | const string = "File " + path + " is not found" + (assertString.length > 0 ? ": " + assertString : ""); 62 | if (!soft) { 63 | throw new Error(string); 64 | } 65 | console.warn(string); 66 | return false; 67 | } 68 | } catch (e) { 69 | return false; 70 | } 71 | return true; 72 | } 73 | 74 | 75 | export function ensureStringLength(str: string, requiredLength: number, fillWith: string = "0"): string { 76 | if (str.length < requiredLength) { 77 | return str.padStart(requiredLength, fillWith); 78 | } else if (str.length > requiredLength) { 79 | return str.slice(0, requiredLength); 80 | } 81 | return str; 82 | } 83 | 84 | 85 | export function withResolvers() { 86 | let resolve: any; 87 | let reject: any; 88 | const promise = new Promise((res, rej) => { 89 | resolve = res; 90 | reject = rej; 91 | }); 92 | return { promise, resolve, reject }; 93 | } 94 | 95 | 96 | export function splitBufferBy(buffer: Buffer, size: number): Buffer[] { 97 | const chunks = []; 98 | for (let i = 0; i < buffer.length; i += size) { 99 | chunks.push(buffer.slice(i, i + size)); 100 | } 101 | return chunks; 102 | } -------------------------------------------------------------------------------- /src/provider/BaseProvider.ts: -------------------------------------------------------------------------------- 1 | import DICloudApp from "../DICloudApp"; 2 | import MutableBuffer from "../helper/MutableBuffer"; 3 | 4 | import { createVFile, IFile } from "../file/IFile"; 5 | import { PassThrough, pipeline, Readable, Transform, Writable } from "stream"; 6 | import { gcm } from '@noble/ciphers/aes'; 7 | import { Cipher, utf8ToBytes } from '@noble/ciphers/utils'; 8 | import { withResolvers } from "../helper/utils"; 9 | 10 | import Log from "../Log"; 11 | export interface IDelayedDeletionEntry { 12 | channel: string; 13 | message: string; 14 | } 15 | 16 | export default abstract class BaseProvider { 17 | private _app: DICloudApp; 18 | private fileDeletionQueue: Array = []; 19 | 20 | public constructor(app: DICloudApp) { 21 | this._app = app; 22 | } 23 | 24 | public get client() { 25 | return this._app; 26 | } 27 | 28 | public addToDeletionQueue(info: IDelayedDeletionEntry[]) { 29 | this.fileDeletionQueue.push(...info); 30 | } 31 | 32 | 33 | public get deletionQueue() { 34 | return this.fileDeletionQueue; 35 | } 36 | 37 | 38 | private createCipher(iv: Uint8Array): Cipher { 39 | const key = utf8ToBytes(this.client.getEncryptPassword()); 40 | return gcm(key, iv); 41 | } 42 | 43 | private async createReadStreamWithDecryption(file: IFile): Promise { 44 | const readStream = await this.createRawReadStream(file); 45 | const decipher = this.createCipher(file.iv); 46 | const decryptedRead = new PassThrough(); 47 | 48 | const encryptedChunkSize = this.calculateSavedFileSize(); 49 | const buffer = new MutableBuffer(encryptedChunkSize); 50 | 51 | 52 | readStream.on("data", (chunk) => { 53 | try { 54 | const left = encryptedChunkSize - buffer.size; 55 | 56 | if (chunk.length <= left) { 57 | buffer.write(chunk); 58 | } else { 59 | buffer.write(chunk.subarray(0, left)); 60 | const decrypted = decipher.decrypt(buffer.cloneNativeBuffer()); 61 | const writeSuccess = decryptedRead.write(decrypted); 62 | if (!writeSuccess) { 63 | readStream.pause(); 64 | } 65 | buffer.clear(); 66 | buffer.write(chunk.subarray(left)); 67 | } 68 | } catch (err) { 69 | decryptedRead.destroy(err instanceof Error ? err : new Error(String(err))); 70 | buffer.destroy(); 71 | } 72 | }); 73 | 74 | readStream.on("end", () => { 75 | try { 76 | if (buffer.size > 0) { 77 | const decrypted = decipher.decrypt(buffer.cloneNativeBuffer()); 78 | decryptedRead.write(decrypted); 79 | } 80 | buffer.destroy(); 81 | decryptedRead.end(); 82 | } catch (err) { 83 | decryptedRead.destroy(err instanceof Error ? err : new Error(String(err))); 84 | buffer.destroy(); 85 | } 86 | }); 87 | 88 | decryptedRead.on("drain", () => { 89 | readStream.resume(); 90 | }); 91 | 92 | readStream.on("error", (err) => { 93 | decryptedRead.destroy(err); 94 | buffer.destroy(); 95 | }); 96 | 97 | decryptedRead.on("error", (err) => { 98 | readStream.destroy(err); 99 | buffer.destroy(); 100 | }); 101 | 102 | return decryptedRead; 103 | } 104 | 105 | 106 | private async createWriteStreamWithEncryption(file: IFile): Promise { 107 | const rawWriteStream = await this.createRawWriteStream(file); 108 | const cipher = this.createCipher(file.iv); 109 | const writeStreamAwaiter = withResolvers(); 110 | 111 | let buffer = new MutableBuffer(this.calculateProviderMaxSize()); 112 | 113 | rawWriteStream.on("finish", () => { 114 | writeStreamAwaiter.resolve(); 115 | }); 116 | 117 | rawWriteStream.on("error", (err) => { 118 | writeStreamAwaiter.reject(err); 119 | buffer.destroy(); 120 | }); 121 | 122 | return new Writable({ 123 | write: async (chunk: Buffer, encoding, callback) => { 124 | const left = this.calculateProviderMaxSize() - buffer.size; 125 | if (chunk.length <= left) { 126 | buffer.write(chunk, encoding); 127 | } else { 128 | buffer.write(chunk.subarray(0, left), encoding); 129 | const f = buffer.flush(); 130 | const e = cipher.encrypt(f); 131 | rawWriteStream.write(e); 132 | buffer.clear(); 133 | buffer.write(chunk.subarray(left), encoding); 134 | } 135 | callback(); 136 | }, 137 | final: async (callback) => { 138 | Log.info("[BaseProvider] final() Finalizing upload."); 139 | if (buffer.size > 0) { 140 | rawWriteStream.write(cipher.encrypt(buffer.flushAndDestory())); 141 | } 142 | rawWriteStream.end(); 143 | await writeStreamAwaiter.promise; // we have to wait for rawWriteStream to finish, otherwise client will close connection too early thinking that upload is finished 144 | callback(); 145 | }, 146 | destroy: (err, callback) => { 147 | Log.info("[BaseProvider] destroy() Destroying write stream (error: " + err + ")"); 148 | buffer.destroy(); 149 | callback(err); 150 | } 151 | }); 152 | 153 | } 154 | 155 | 156 | 157 | /** 158 | * Method that should be used to implement queue for deleting files from provider. Queue is used to prevent ratelimiting and other blocking issues. 159 | */ 160 | public abstract processDeletionQueue(): Promise; 161 | 162 | /** 163 | * Method that should provide raw read stream for downloading files from provider. Only basic read stream from provider, no decryption or anything else. 164 | * @param file - File which should be downloaded. 165 | */ 166 | public abstract createRawReadStream(file: IFile): Promise; 167 | /** 168 | * Method that should provide raw write stream for uploading files to provider. Only basic write stream to provider, no encryption or anything else. 169 | * @param file - File which should be uploaded. 170 | * @param callbacks - Callbacks for write stream. 171 | */ 172 | public abstract createRawWriteStream(file: IFile): Promise; 173 | 174 | 175 | /** 176 | * Custom provider should implement this method to provide max file size. 177 | */ 178 | abstract calculateProviderMaxSize(): number; 179 | abstract calculateSavedFileSize(): number; 180 | 181 | 182 | /* ----------------------------------------------------------------------------------------- */ 183 | 184 | 185 | /** 186 | * Main method that should be used to download files from provider. 187 | * Creates read stream for downloading files from provider. Handles decryption if enabled. 188 | * Does not handle with any fs operations, only downloads from provider. 189 | * @param file 190 | * @returns 191 | */ 192 | async createReadStream(file: IFile): Promise { 193 | if (file.encrypted) { 194 | return await this.createReadStreamWithDecryption(file); 195 | } 196 | 197 | return await this.createRawReadStream(file); 198 | } 199 | 200 | /** 201 | * Main method that should be used to upload files to provider. 202 | * Creates write stream for uploading files to provider. Handles encryption if enabled. 203 | * Does not handle with any fs operations, only uploads to provider. 204 | * Mutates the file object (chunks and size) 205 | * @param file - file to upload 206 | * @param callbacks - callbacks for write stream. 207 | * @returns write stream 208 | */ 209 | async createWriteStream(file: IFile): Promise { 210 | if (file.encrypted) { 211 | return await this.createWriteStreamWithEncryption(file); 212 | } 213 | 214 | return await this.createRawWriteStream(file); 215 | } 216 | 217 | /** 218 | * Convinient buffer upload function 219 | * @param buffer Buffer with file data 220 | * @param name Filename. Not really used, but can be used for logging or other purposes. 221 | * @returns created file struct with all data about file. 222 | */ 223 | public async uploadFile(buffer: Buffer, name: string): Promise { 224 | const file = createVFile(name, 0, this.client.shouldEncryptFiles()); 225 | const stream = await this.createWriteStream(file); 226 | 227 | return new Promise(async (resolve, reject) => { 228 | stream.on("finish", () => { 229 | resolve(file); 230 | }); 231 | 232 | stream.on("error", (err) => { 233 | reject(err); 234 | }); 235 | Readable.from(buffer).pipe(stream); 236 | }); 237 | } 238 | 239 | /** 240 | * Convinient download function that downloads file from provider and returns it as buffer. 241 | * @param file valid file struct 242 | * @returns Buffer with file data 243 | */ 244 | public async downloadFile(file: IFile): Promise { 245 | const stream = await this.createReadStream(file); 246 | const size = file.encrypted ? file.size - (16 * file.chunks.length) : file.size; 247 | 248 | return new Promise((resolve, reject) => { 249 | const buffer = new MutableBuffer(size); 250 | stream.on("data", (chunk) => { 251 | buffer.write(chunk) 252 | }); 253 | 254 | stream.on("end", () => { 255 | resolve(buffer.flushAndDestory()); 256 | }); 257 | 258 | stream.on("error", (err) => { 259 | buffer.destroy(); 260 | reject(err); 261 | }); 262 | }); 263 | } 264 | 265 | } -------------------------------------------------------------------------------- /src/provider/DiscordFileProvider.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import BaseProvider from "./BaseProvider.js"; 3 | import HttpStreamPool from '../HttpStreamPool.js'; 4 | 5 | import { AttachmentBuilder, TextChannel } from "discord.js"; 6 | import { Writable, Readable, PassThrough } from "stream"; 7 | import { splitBufferBy, truncate } from "../helper/utils.js"; 8 | import { IFile } from "../file/IFile.js"; 9 | 10 | import Log from "../Log.js"; 11 | import MutableBuffer from "../helper/MutableBuffer.js"; 12 | 13 | export const MAX_MB_CHUNK_SIZE = 10; // megabytes chunk size. Discord allows 10MB per file. 14 | export const ENCRYPTION_OVERHEAD = 16; // 16 bytes for encryption metadata 15 | 16 | export const MAX_CHUNK_SIZE = (MAX_MB_CHUNK_SIZE * 1000 * 1000) - ENCRYPTION_OVERHEAD; 17 | 18 | export default class DiscordFileProvider extends BaseProvider { 19 | 20 | private getAttachmentBuilderFromBuffer(stream: Buffer, chunkName: string, chunkNumber: number = 0, addExtension: boolean = false, encrypt: boolean, extension: string = "txt"): AttachmentBuilder { 21 | return new AttachmentBuilder(stream, { 22 | name: (chunkNumber ? chunkNumber + "-" : "") + chunkName + (addExtension ? "." + extension : "") + (encrypt ? ".enc" : "") 23 | }); 24 | } 25 | 26 | private async uploadChunkToDiscord(buf: MutableBuffer, chunkNumber: number, filesChannel: TextChannel, file: IFile) { 27 | Log.info(`[${file.name}] Uploading chunk ${chunkNumber}....`); 28 | const size = buf.size; 29 | const message = await filesChannel.send({ 30 | files: [ 31 | this.getAttachmentBuilderFromBuffer( 32 | buf.flush(), 33 | path.parse(truncate(file.name, 15)).name, 34 | chunkNumber, 35 | false, 36 | file.encrypted 37 | ) 38 | ], 39 | }); 40 | 41 | file.chunks.push({ 42 | id: message.id, 43 | size, 44 | }); 45 | Log.info(`[${file.name}] Chunk ${chunkNumber} added.`); 46 | } 47 | 48 | public async createRawReadStream(file: IFile): Promise { 49 | Log.info(".createRawReadStream() - file: " + file.name); 50 | return (await (new HttpStreamPool(file).getDownloadStream(async (id) => { 51 | return (await this.client.getFilesChannel().messages.fetch(id)).attachments.first()!.url; 52 | }))); 53 | } 54 | 55 | /** 56 | * Updates the file size and chunk size. 57 | */ 58 | public async createRawWriteStream(file: IFile): Promise { 59 | Log.info(".createRawWriteStream() - file: " + file.name); 60 | 61 | const channel = this.client.getFilesChannel(); 62 | let chunkId = 1; 63 | let chunkBuffer = new MutableBuffer(); 64 | let totalFileSize = 0; 65 | 66 | const uploadStream = new Writable({ 67 | write: async (chunk_: Buffer, encoding: BufferEncoding, callback) => { 68 | for (const chunk of splitBufferBy(chunk_, MAX_CHUNK_SIZE)) { // in case the chunk is too big 69 | if(chunkBuffer.size + chunk.length > MAX_CHUNK_SIZE) { 70 | const left = MAX_CHUNK_SIZE - chunkBuffer.size; 71 | const slice = chunk.slice(0, left); 72 | chunkBuffer.write(slice, encoding); 73 | await this.uploadChunkToDiscord(chunkBuffer, chunkId, channel, file); 74 | chunkBuffer.destroy(); 75 | chunkBuffer = new MutableBuffer(); 76 | chunkBuffer.write(chunk.slice(left), encoding); 77 | chunkId++; 78 | } else { 79 | chunkBuffer.write(chunk); 80 | } 81 | totalFileSize += chunk.length; 82 | } 83 | callback(); 84 | }, 85 | final: async (callback) => { 86 | Log.info("[DiscordProvider] final() Finalizing upload."); 87 | if (chunkBuffer.size > 0) { 88 | await this.uploadChunkToDiscord(chunkBuffer, chunkId, channel, file); 89 | chunkBuffer.destroy(); 90 | } 91 | file.size = totalFileSize; 92 | callback(); 93 | } 94 | }); 95 | 96 | return uploadStream; 97 | } 98 | 99 | public async processDeletionQueue(): Promise { 100 | if (this.deletionQueue.length > 0) { 101 | const info = this.deletionQueue.shift()!; 102 | const channel = this.client.getDiscordClient().channels.cache.get(info.channel) as TextChannel; 103 | 104 | if (!channel) { 105 | Log.error("Failed to find channel: " + info.channel); 106 | return; 107 | } 108 | try { 109 | await channel.messages.delete(info.message); 110 | } catch (e) { 111 | Log.error(e); 112 | Log.error("Failed to delete message: " + info.message + " in channel: " + info.channel); 113 | } 114 | } 115 | } 116 | 117 | calculateProviderMaxSize(): number { 118 | return MAX_CHUNK_SIZE; 119 | } 120 | 121 | calculateSavedFileSize(): number { 122 | return MAX_CHUNK_SIZE + ENCRYPTION_OVERHEAD; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/webdav/WebdavFileSystem.ts: -------------------------------------------------------------------------------- 1 | import { Readable, Writable } from "stream"; 2 | import { ResourceType, v2, Errors } from "webdav-server"; 3 | import { LocalLockManager, LocalPropertyManager, Resource } from "webdav-server/lib/index.v2.js"; 4 | 5 | 6 | import mime from "mime-types"; 7 | import path from "path"; 8 | import { createVFile } from "../file/IFile"; 9 | 10 | import DICloudApp from "../DICloudApp.js"; 11 | import VolumeEx from "../file/VolumeEx.js"; 12 | import Log from "../Log.js"; 13 | import { ENCRYPTION_OVERHEAD } from "../provider/DiscordFileProvider"; 14 | 15 | 16 | class VirtualDiscordFileSystemSerializer implements v2.FileSystemSerializer { 17 | uid(): string { return "virtual-discord-file-system@1.0.0"; } 18 | serialize(fs: v2.FileSystem, callback: v2.ReturnCallback): void { throw new Error("Method not implemented."); } 19 | unserialize(serializedData: any, callback: v2.ReturnCallback): void { throw new Error("Method not implemented."); } 20 | } 21 | 22 | export default class DiscordWebdavFilesystemHandler extends v2.FileSystem { 23 | private client: DICloudApp; 24 | private fs: VolumeEx; 25 | 26 | private locks: Map = new Map(); 27 | private properties: Map = new Map(); 28 | 29 | constructor(client: DICloudApp) { 30 | super(new VirtualDiscordFileSystemSerializer()); 31 | this.client = client; 32 | this.fs = client.getFs(); 33 | } 34 | 35 | 36 | protected _lockManager(path: v2.Path, ctx: v2.LockManagerInfo, callback: v2.ReturnCallback): void { 37 | return callback(undefined, this.createOrGetLockManager(path)); 38 | } 39 | 40 | protected _propertyManager(path: v2.Path, ctx: v2.PropertyManagerInfo, callback: v2.ReturnCallback): void { 41 | return callback(undefined, this.getOrCreatePropManager(path)); 42 | } 43 | 44 | private createOrGetLockManager(path: v2.Path): LocalLockManager { 45 | if (!this.locks.has(path.toString())) { 46 | this.locks.set(path.toString(), new LocalLockManager()); 47 | } 48 | 49 | return this.locks.get(path.toString())!; 50 | } 51 | 52 | private getOrCreatePropManager(path: v2.Path): LocalPropertyManager { 53 | if (!this.properties.has(path.toString())) { 54 | this.properties.set(path.toString(), new LocalPropertyManager()); 55 | } 56 | 57 | return this.properties.get(path.toString())!; 58 | } 59 | 60 | private cleanupLocksAndProperties(path: v2.Path) { 61 | this.locks.delete(path.toString()); 62 | this.properties.delete(path.toString()); 63 | } 64 | 65 | /** 66 | * Returns the mime type of the file according to the file extension. (Not by the file content) 67 | * @param filename 68 | * @returns 69 | */ 70 | private getMimeType(rPath: string): string { 71 | return mime.lookup(path.parse(rPath).base) || "application/octet-stream"; 72 | } 73 | 74 | // private valiatePath(path: v2.Path, callback: v2.ReturnCallback): void { 75 | // if (!this.fs.existsSync(path.toString())) { 76 | // return callback(Errors.ResourceNotFound); 77 | // } 78 | // } 79 | 80 | 81 | protected _size(path: v2.Path, ctx: v2.SizeInfo, callback: v2.ReturnCallback): void { 82 | Log.info(".size", path.toString(), getContext(ctx)); 83 | 84 | const stat = this.fs.statSync(path.toString()); 85 | 86 | if (stat.isFile()) { 87 | const file = this.fs.getFile(path.toString()); 88 | 89 | if(file.encrypted){ 90 | return callback(undefined, file.size - (ENCRYPTION_OVERHEAD * file.chunks.length)); // -16 bytes for each chunk for encryption metadata. client side will wait for full file, so if we provide size with metadata, which exists only on the server, the client will wait for the metadata to be downloaded which will never happen. 91 | } 92 | return callback(undefined, file.size); 93 | } 94 | return callback(undefined, this.fs.getTreeSizeRecursive(path.toString())); 95 | } 96 | 97 | protected _readDir(path: v2.Path, ctx: v2.ReadDirInfo, callback: v2.ReturnCallback): void { 98 | Log.info(".readDir", path.toString(), getContext(ctx)); 99 | 100 | const stat = this.fs.statSync(path.toString()); 101 | 102 | if (stat.isDirectory()) { 103 | return callback(undefined, this.fs.readdirSync(path.toString()) as string[]); 104 | } 105 | 106 | return callback(Errors.ResourceNotFound); 107 | } 108 | 109 | protected _type(path: v2.Path, ctx: v2.TypeInfo, callback: v2.ReturnCallback): void { 110 | // Log.info(".type", path.toString(), getContext(ctx)); 111 | 112 | const stat = this.fs.statSync(path.toString()); 113 | 114 | if (stat.isFile()) { 115 | return callback(undefined, ResourceType.File); 116 | } else if (stat.isDirectory()) { 117 | return callback(undefined, ResourceType.Directory); 118 | } 119 | 120 | return callback(undefined, ResourceType.NoResource); 121 | } 122 | 123 | protected _mimeType(path: v2.Path, ctx: v2.MimeTypeInfo, callback: v2.ReturnCallback): void { 124 | Log.info(".mimeType", path.toString(), getContext(ctx)); 125 | const stat = this.fs.statSync(path.toString()); 126 | 127 | if (stat.isFile()) { 128 | return callback(undefined, this.getMimeType(path.toString())); 129 | } 130 | 131 | return callback(Errors.NoMimeTypeForAFolder); 132 | } 133 | 134 | protected _fastExistCheck(ctx: v2.RequestContext, path: v2.Path, callback: (exists: boolean) => void): void { 135 | // Log.info(".fastExistCheck", path.toString(), getContext(ctx)); 136 | 137 | return callback(this.fs.existsSync(path.toString())); 138 | } 139 | 140 | _create(path: v2.Path, ctx: v2.CreateInfo, callback: v2.SimpleCallback): void { 141 | Log.info(".create", path.toString(), getContext(ctx)); 142 | 143 | if (ctx.type.isDirectory) { 144 | this.fs.mkdirSync(path.toString(), { recursive: true }); 145 | } 146 | 147 | if (ctx.type.isFile) { 148 | this.fs.setFile(path.toString(), createVFile(path.fileName(), 0, this.client.shouldEncryptFiles())); 149 | } 150 | 151 | this.client.markForUpload(); 152 | return callback(); 153 | } 154 | 155 | 156 | // called on file download. 157 | async _openReadStream(path: v2.Path, ctx: v2.OpenReadStreamInfo, callback: v2.ReturnCallback): Promise { 158 | Log.info(".openReadStream (path, estimatedSize, ctx)", path.toString(), ctx.estimatedSize, getContext(ctx)); 159 | 160 | const stat = this.fs.statSync(path.toString()); 161 | 162 | if (!stat.isFile()){ 163 | return callback(Errors.ResourceNotFound); 164 | } 165 | 166 | const file = this.fs.getFile(path.toString()) 167 | if (file.size == 0 || file.chunks.length == 0) { 168 | Log.info(".openReadStream", "File has no chunks: " + path.toString()); 169 | return callback(undefined, Readable.from([])); 170 | } 171 | 172 | Log.info(".openReadStream, creating read stream: ", file); 173 | const readStream = await this.client.getProvider().createReadStream(file) 174 | Log.info(".openReadStream", "Stream opened: " + path.toString()); 175 | return callback(undefined, readStream); 176 | } 177 | 178 | 179 | async _openWriteStream(path: v2.Path, ctx: v2.OpenWriteStreamInfo, callback: v2.ReturnCallback): Promise { 180 | const { targetSource, estimatedSize, mode } = ctx; 181 | Log.info(".openWriteStream", targetSource, estimatedSize, mode); 182 | 183 | const stat = this.fs.statSync(path.toString()); 184 | 185 | if (!stat.isFile()) { 186 | return callback(Errors.InvalidOperation); 187 | } 188 | 189 | const file = this.fs.getFile(path.toString()); 190 | 191 | // overwrite file 192 | this.client.getProvider().addToDeletionQueue(file.chunks.map(chunk => ({ 193 | channel: this.client.getFilesChannel().id, 194 | message: chunk.id 195 | }))); 196 | 197 | file.chunks = []; 198 | file.modified = new Date(); 199 | this.fs.setFile(path.toString(), file); 200 | this.client.markForUpload(); 201 | 202 | 203 | const writeStream = await this.client.getProvider().createWriteStream(file); 204 | 205 | writeStream.on("finish", () => { 206 | Log.info(".openWriteStream", "Stream finished: " + path.toString()); 207 | this.fs.setFile(path.toString(), file); 208 | this.client.markForUpload(); 209 | }); 210 | 211 | writeStream.on("error", (err) => { 212 | Log.info(".openWriteStream", "Stream error: " + path.toString() + " | " + err); 213 | this.fs.rmSync(path.toString(), { recursive: true }); 214 | }); 215 | Log.info(".openWriteStream", "Stream opened: " + path.toString()); 216 | 217 | return callback(undefined, writeStream); 218 | } 219 | 220 | 221 | async _delete(path: v2.Path, ctx: v2.DeleteInfo, callback: v2.SimpleCallback): Promise { 222 | Log.info(".delete", path.toString(), getContext(ctx)); 223 | 224 | if (path.toString() == "/") { 225 | return callback(Errors.InvalidOperation); 226 | } 227 | 228 | const stat = this.fs.statSync(path.toString()); 229 | const filesToDelete: string[] = []; 230 | 231 | if (stat.isFile()) { 232 | filesToDelete.push(path.toString()); 233 | } 234 | 235 | if (stat.isDirectory()) { 236 | filesToDelete.push(...this.fs.getPathsRecursive(path.toString())); 237 | } 238 | 239 | for (const fileToDelete of filesToDelete) { 240 | for (const chunk of this.fs.getFile(fileToDelete).chunks) { 241 | this.client.getProvider().addToDeletionQueue([{ 242 | channel: this.client.getFilesChannel().id, 243 | message: chunk.id 244 | }]); 245 | } 246 | } 247 | 248 | this.fs.rmSync(path.toString(), { recursive: true }); 249 | this.client.markForUpload(); 250 | 251 | for (const fileToDelete of filesToDelete) { 252 | this.cleanupLocksAndProperties(new v2.Path(fileToDelete)); 253 | } 254 | this.cleanupLocksAndProperties(path); 255 | 256 | 257 | return callback(); 258 | } 259 | 260 | /** 261 | * Copies a file from pathFrom to pathTo. Automatically marks the client as dirty and updates the file system. 262 | * @param pathFrom 263 | * @param pathTo 264 | * @returns 265 | */ 266 | private copyFile(pathFrom: v2.Path, pathTo: v2.Path): Promise { 267 | return new Promise(async (resolve, reject) => { 268 | if (!this.fs.existsSync(pathFrom.toString()) || pathFrom.toString() == pathTo.toString()) { 269 | return resolve(false); 270 | } 271 | 272 | this.fs.mkdirSync(path.parse(pathTo.toString()).dir, { recursive: true }); 273 | 274 | const oldFile = this.fs.getFile(pathFrom.toString()); 275 | const newFile = createVFile(pathTo.fileName(), oldFile.size, oldFile.encrypted); 276 | 277 | const readStream = await this.client.getProvider().createReadStream(oldFile); 278 | const writeStream = await this.client.getProvider().createWriteStream(newFile); 279 | 280 | writeStream.on("error", (err) => { 281 | Log.info(".copy", "Stream error: " + pathTo.toString() + " | " + err); 282 | return reject(false); 283 | }); 284 | 285 | writeStream.on("finish", () => { 286 | Log.info(".copy", "Stream finished: " + pathTo.toString()); 287 | this.fs.setFile(pathTo.toString(), newFile); 288 | 289 | this.locks.set(pathTo.toString(), new LocalLockManager()); 290 | const oldProps = this.properties.get(pathFrom.toString()); 291 | if (oldProps) { 292 | this.properties.set(pathTo.toString(), oldProps); 293 | } 294 | 295 | 296 | this.client.markForUpload(); 297 | 298 | return resolve(true); 299 | }); 300 | 301 | readStream.pipe(writeStream); 302 | }); 303 | } 304 | 305 | // serverside copy 306 | async _copy(pathFrom: v2.Path, pathTo: v2.Path, ctx: v2.CopyInfo, callback: v2.ReturnCallback): Promise { 307 | Log.info(".copy", pathFrom + " | " + pathTo); 308 | 309 | const sourceExists = this.fs.existsSync(pathFrom.toString()); 310 | const targetExists = this.fs.existsSync(pathTo.toString()); 311 | 312 | if (!sourceExists || targetExists) { 313 | return callback(Errors.Forbidden); 314 | } 315 | 316 | const sourceStat = this.fs.statSync(pathFrom.toString()); 317 | 318 | if (sourceStat.isDirectory()) { 319 | let files = this.fs.getFilesWithPathRecursive(pathFrom.toString()); 320 | for (let oldPath in files) { 321 | let newPath = pathTo.toString() + oldPath.substr(pathFrom.toString().length); 322 | if (!await this.copyFile(new v2.Path(oldPath), new v2.Path(newPath))) { 323 | return callback(Errors.InvalidOperation); 324 | } 325 | } 326 | } 327 | 328 | if (sourceStat.isFile()) { 329 | if (!await this.copyFile(pathFrom, pathTo)) { 330 | return callback(Errors.InvalidOperation); 331 | } 332 | } 333 | 334 | return callback(undefined, true); 335 | } 336 | 337 | async _move(pathFrom: v2.Path, pathTo: v2.Path, ctx: v2.MoveInfo, callback: v2.ReturnCallback): Promise { 338 | Log.info(".move", pathFrom.toString(), pathTo.toString(), getContext(ctx)); 339 | 340 | const sourceExists = this.fs.existsSync(pathFrom.toString()); 341 | const targetExists = this.fs.existsSync(pathTo.toString()); 342 | 343 | if (!sourceExists || (targetExists && !ctx.overwrite)) { 344 | return callback(Errors.InvalidOperation); 345 | } 346 | 347 | 348 | if(ctx.overwrite && targetExists){ 349 | const targetStat = this.fs.statSync(pathTo.toString()); 350 | if(targetStat.isFile()){ 351 | this.fs.rmSync(pathTo.toString(), { force: true }); 352 | } else { 353 | return callback(Errors.InvalidOperation); 354 | } 355 | } 356 | 357 | this.fs.renameSync(pathFrom.toString(), pathTo.toString()); 358 | this.client.markForUpload(); 359 | 360 | this.locks.set(pathTo.toString(), new LocalLockManager()); 361 | const oldProps = this.properties.get(pathFrom.toString()); 362 | if (oldProps) { 363 | this.properties.set(pathTo.toString(), oldProps); 364 | } 365 | this.cleanupLocksAndProperties(pathFrom); 366 | 367 | return callback(undefined, true); 368 | } 369 | 370 | _rename(pathFrom: v2.Path, newName: string, ctx: v2.RenameInfo, callback: v2.ReturnCallback): void { 371 | Log.info(".rename", pathFrom.toString(), newName, getContext(ctx)); 372 | 373 | const oldPath = pathFrom.toString(); 374 | const newPath = pathFrom.parentName() + "/" + newName; 375 | 376 | if (!this.fs.existsSync(oldPath)) { 377 | return callback(Errors.ResourceNotFound); 378 | } 379 | 380 | if (this.fs.existsSync(newPath)) { 381 | return callback(Errors.ResourceAlreadyExists); 382 | } 383 | 384 | this.locks.set(newPath, new LocalLockManager()); 385 | const oldProps = this.properties.get(oldPath); 386 | if (oldProps) { 387 | this.properties.set(newPath, oldProps); 388 | } 389 | this.cleanupLocksAndProperties(pathFrom); 390 | 391 | 392 | this.fs.renameSync(oldPath, newPath); 393 | this.client.markForUpload(); 394 | 395 | callback(undefined, true); 396 | } 397 | 398 | protected _lastModifiedDate(path: v2.Path, ctx: v2.LastModifiedDateInfo, callback: v2.ReturnCallback): void { 399 | Log.info(".lastModifiedDate", path.toString(), getContext(ctx)); 400 | 401 | if (this.fs.statSync(path.toString()).isDirectory()) { 402 | return callback(undefined, new Date().getTime()); 403 | } 404 | 405 | const file = this.fs.getFile(path.toString()); 406 | return callback(undefined, file.modified.getTime()); 407 | } 408 | 409 | protected _creationDate(path: v2.Path, ctx: v2.CreationDateInfo, callback: v2.ReturnCallback): void { 410 | // Log.info(".creationDate", path.toString(), getContext(ctx)); 411 | 412 | if (this.fs.statSync(path.toString()).isDirectory()) { 413 | return callback(undefined, new Date().getTime()); 414 | } 415 | 416 | const file = this.fs.getFile(path.toString()); 417 | return callback(undefined, file.created.getTime()); 418 | } 419 | 420 | 421 | protected _etag(path: v2.Path, ctx: v2.ETagInfo, callback: v2.ReturnCallback): void { 422 | // Log.info(".etag", path.toString()); 423 | 424 | const stat = this.fs.statSync(path.toString()); 425 | 426 | if (stat.isDirectory()) { 427 | return callback(undefined, "0"); 428 | } 429 | 430 | return callback(undefined, this.fs.getFile(path.toString()).modified.getTime().toString()); 431 | } 432 | 433 | } 434 | 435 | function getContext(ctx: v2.IContextInfo) { 436 | return { 437 | host: ctx.context.headers.host, 438 | contentLength: ctx.context.headers.contentLength, 439 | useragent: ctx.context.headers.find("user-agent", "unkown useragent"), 440 | uri: ctx.context.requested.uri, 441 | } 442 | } -------------------------------------------------------------------------------- /src/webdav/WebdavServer.ts: -------------------------------------------------------------------------------- 1 | import { v2 as webdav } from "webdav-server"; 2 | import { Server, IncomingMessage, ServerResponse } from "http"; 3 | import DICloudApp from "../DICloudApp"; 4 | export interface IUserData { 5 | username: string; 6 | password: string; 7 | } 8 | 9 | export interface ServerOptions extends webdav.WebDAVServerOptions { 10 | users?: IUserData[]; 11 | enableAuth?: boolean; 12 | }; 13 | 14 | export default class WebdavServer extends webdav.WebDAVServer { 15 | private app: DICloudApp; 16 | 17 | private constructor(options: ServerOptions, app: DICloudApp) { 18 | super(options); 19 | this.app = app; 20 | // this.setupBeforeRequestHandler(); 21 | } 22 | 23 | public static createServer(options: ServerOptions, app: DICloudApp): WebdavServer { 24 | const userManager = new webdav.SimpleUserManager(); 25 | const privManager = new webdav.SimplePathPrivilegeManager(); 26 | 27 | if (options.users) { 28 | options.users.forEach((user) => { 29 | privManager.setRights(userManager.addUser(user.username, user.password, true), "/", ["all"]); 30 | }); 31 | } 32 | 33 | options.hostname = options.hostname || "0.0.0.0"; 34 | 35 | return new WebdavServer({ 36 | privilegeManager: options.enableAuth ? privManager : undefined, 37 | httpAuthentication: options.enableAuth ? new webdav.HTTPDigestAuthentication(userManager, "DICloud Server") : undefined, 38 | ...options, 39 | }, app); 40 | } 41 | 42 | async startAsync(): Promise> { 43 | return super.startAsync(this.options.port!); 44 | } 45 | 46 | 47 | 48 | } -------------------------------------------------------------------------------- /tests/raw.spec.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | dotenv.config(); 3 | 4 | import * as uvu from 'uvu'; 5 | import * as assert from 'uvu/assert'; 6 | 7 | import { envBoot } from "../bootloader" 8 | import DICloudApp from '../src/DICloudApp'; 9 | 10 | const test = uvu.test; 11 | 12 | let app: DICloudApp; 13 | let encryptionOffset = 0; 14 | 15 | async function sleep(ms: number) { 16 | return new Promise(resolve => setTimeout(resolve, ms)); 17 | } 18 | 19 | 20 | test('server boot', async () => { 21 | app = await envBoot(); 22 | 23 | if (app.shouldEncryptFiles()) { 24 | encryptionOffset = 16 // hardcoded for now, TODO: get later 25 | } 26 | 27 | assert.ok(app); 28 | }); 29 | 30 | 31 | test("create and read small file", async () => { 32 | const data = { 33 | name: "test-small.txt", 34 | content: "Hello, World!", 35 | size: 0 36 | } 37 | data.size = Buffer.byteLength(data.content); 38 | 39 | const buffer = Buffer.from(data.content); 40 | const uploadedFile = await app.uploadFile(buffer, data.name); 41 | assert.is(uploadedFile.size - encryptionOffset, data.size); 42 | 43 | const downloadedBuffer = await app.downloadFile(uploadedFile); 44 | assert.is(downloadedBuffer.toString(), data.content); 45 | }); 46 | 47 | test("create and read big", async () => { 48 | const fileSize = 15_000_000; // 15MB 49 | 50 | const data = { 51 | name: "test-big.txt", 52 | content: Buffer.alloc(fileSize, 0).toString(), 53 | size: 0 54 | } 55 | data.size = Buffer.byteLength(data.content); 56 | 57 | const buffer = Buffer.from(data.content); 58 | const uploadedFile = await app.uploadFile(buffer, data.name); 59 | assert.is(uploadedFile.size - (encryptionOffset * uploadedFile.chunks.length), data.size); 60 | 61 | const downloadedBuffer = await app.downloadFile(uploadedFile); 62 | assert.is(downloadedBuffer.toString(), data.content); 63 | }); 64 | 65 | test("create and read empty file", async () => { 66 | const data = { 67 | name: "empty.txt", 68 | content: "", 69 | size: 0 70 | } 71 | data.size = Buffer.byteLength(data.content); 72 | 73 | const buffer = Buffer.from(data.content); 74 | const uploadedFile = await app.uploadFile(buffer, data.name); 75 | assert.is(uploadedFile.size, 0); 76 | 77 | const downloadedBuffer = await app.downloadFile(uploadedFile); 78 | assert.is(downloadedBuffer.toString(), data.content); 79 | 80 | }); 81 | 82 | 83 | 84 | test.run(); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2023", 4 | "module": "CommonJS", 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "typeRoots": [ 9 | "./types" 10 | ], 11 | "outDir": "./out", 12 | "esModuleInterop": true, 13 | "skipLibCheck": true, 14 | "sourceMap": true 15 | }, 16 | } -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@discordjs/builders@^1.9.0": 6 | version "1.9.0" 7 | resolved "https://registry.yarnpkg.com/@discordjs/builders/-/builders-1.9.0.tgz#71fa6de91132bd1deaff2a9daea7aa5d5c9f124a" 8 | integrity sha512-0zx8DePNVvQibh5ly5kCEei5wtPBIUbSoE9n+91Rlladz4tgtFbJ36PZMxxZrTEOQ7AHMZ/b0crT/0fCy6FTKg== 9 | dependencies: 10 | "@discordjs/formatters" "^0.5.0" 11 | "@discordjs/util" "^1.1.1" 12 | "@sapphire/shapeshift" "^4.0.0" 13 | discord-api-types "0.37.97" 14 | fast-deep-equal "^3.1.3" 15 | ts-mixer "^6.0.4" 16 | tslib "^2.6.3" 17 | 18 | "@discordjs/collection@1.5.3": 19 | version "1.5.3" 20 | resolved "https://registry.yarnpkg.com/@discordjs/collection/-/collection-1.5.3.tgz#5a1250159ebfff9efa4f963cfa7e97f1b291be18" 21 | integrity sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ== 22 | 23 | "@discordjs/collection@^2.1.0", "@discordjs/collection@^2.1.1": 24 | version "2.1.1" 25 | resolved "https://registry.yarnpkg.com/@discordjs/collection/-/collection-2.1.1.tgz#901917bc538c12b9c3613036d317847baee08cae" 26 | integrity sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg== 27 | 28 | "@discordjs/formatters@^0.5.0": 29 | version "0.5.0" 30 | resolved "https://registry.yarnpkg.com/@discordjs/formatters/-/formatters-0.5.0.tgz#2d284c4271bc41984339936df1d0164e470f3b7a" 31 | integrity sha512-98b3i+Y19RFq1Xke4NkVY46x8KjJQjldHUuEbCqMvp1F5Iq9HgnGpu91jOi/Ufazhty32eRsKnnzS8n4c+L93g== 32 | dependencies: 33 | discord-api-types "0.37.97" 34 | 35 | "@discordjs/rest@^2.3.0", "@discordjs/rest@^2.4.0": 36 | version "2.4.0" 37 | resolved "https://registry.yarnpkg.com/@discordjs/rest/-/rest-2.4.0.tgz#63bfc816af58af844914e3589d7eae609cd199b5" 38 | integrity sha512-Xb2irDqNcq+O8F0/k/NaDp7+t091p+acb51iA4bCKfIn+WFWd6HrNvcsSbMMxIR9NjcMZS6NReTKygqiQN+ntw== 39 | dependencies: 40 | "@discordjs/collection" "^2.1.1" 41 | "@discordjs/util" "^1.1.1" 42 | "@sapphire/async-queue" "^1.5.3" 43 | "@sapphire/snowflake" "^3.5.3" 44 | "@vladfrangu/async_event_emitter" "^2.4.6" 45 | discord-api-types "0.37.97" 46 | magic-bytes.js "^1.10.0" 47 | tslib "^2.6.3" 48 | undici "6.19.8" 49 | 50 | "@discordjs/util@^1.1.0", "@discordjs/util@^1.1.1": 51 | version "1.1.1" 52 | resolved "https://registry.yarnpkg.com/@discordjs/util/-/util-1.1.1.tgz#bafcde0faa116c834da1258d78ec237080bbab29" 53 | integrity sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g== 54 | 55 | "@discordjs/ws@1.1.1": 56 | version "1.1.1" 57 | resolved "https://registry.yarnpkg.com/@discordjs/ws/-/ws-1.1.1.tgz#bffbfd46838258ab09054ed98ddef1a36f6507a3" 58 | integrity sha512-PZ+vLpxGCRtmr2RMkqh8Zp+BenUaJqlS6xhgWKEZcgC/vfHLEzpHtKkB0sl3nZWpwtcKk6YWy+pU3okL2I97FA== 59 | dependencies: 60 | "@discordjs/collection" "^2.1.0" 61 | "@discordjs/rest" "^2.3.0" 62 | "@discordjs/util" "^1.1.0" 63 | "@sapphire/async-queue" "^1.5.2" 64 | "@types/ws" "^8.5.10" 65 | "@vladfrangu/async_event_emitter" "^2.2.4" 66 | discord-api-types "0.37.83" 67 | tslib "^2.6.2" 68 | ws "^8.16.0" 69 | 70 | "@isaacs/cliui@^8.0.2": 71 | version "8.0.2" 72 | resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" 73 | integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== 74 | dependencies: 75 | string-width "^5.1.2" 76 | string-width-cjs "npm:string-width@^4.2.0" 77 | strip-ansi "^7.0.1" 78 | strip-ansi-cjs "npm:strip-ansi@^6.0.1" 79 | wrap-ansi "^8.1.0" 80 | wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" 81 | 82 | "@jsonjoy.com/base64@^1.1.1": 83 | version "1.1.2" 84 | resolved "https://registry.yarnpkg.com/@jsonjoy.com/base64/-/base64-1.1.2.tgz#cf8ea9dcb849b81c95f14fc0aaa151c6b54d2578" 85 | integrity sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA== 86 | 87 | "@jsonjoy.com/json-pack@^1.0.3": 88 | version "1.1.0" 89 | resolved "https://registry.yarnpkg.com/@jsonjoy.com/json-pack/-/json-pack-1.1.0.tgz#33ca57ee29d12feef540f2139225597469dec894" 90 | integrity sha512-zlQONA+msXPPwHWZMKFVS78ewFczIll5lXiVPwFPCZUsrOKdxc2AvxU1HoNBmMRhqDZUR9HkC3UOm+6pME6Xsg== 91 | dependencies: 92 | "@jsonjoy.com/base64" "^1.1.1" 93 | "@jsonjoy.com/util" "^1.1.2" 94 | hyperdyperid "^1.2.0" 95 | thingies "^1.20.0" 96 | 97 | "@jsonjoy.com/util@^1.1.2", "@jsonjoy.com/util@^1.3.0": 98 | version "1.3.0" 99 | resolved "https://registry.yarnpkg.com/@jsonjoy.com/util/-/util-1.3.0.tgz#e5623885bb5e0c48c1151e4dae422fb03a5887a1" 100 | integrity sha512-Cebt4Vk7k1xHy87kHY7KSPLT77A7Ev7IfOblyLZhtYEhrdQ6fX4EoLq3xOQ3O/DRMEh2ok5nyC180E+ABS8Wmw== 101 | 102 | "@noble/ciphers@^1.0.0": 103 | version "1.0.0" 104 | resolved "https://registry.yarnpkg.com/@noble/ciphers/-/ciphers-1.0.0.tgz#34758a1cbfcd4126880f83e6b1cdeb88785b7970" 105 | integrity sha512-wH5EHOmLi0rEazphPbecAzmjd12I6/Yv/SiHdkA9LSycsQk7RuuTp7am5/o62qYr0RScE7Pc9icXGBbsr6cesA== 106 | 107 | "@pkgjs/parseargs@^0.11.0": 108 | version "0.11.0" 109 | resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" 110 | integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== 111 | 112 | "@sapphire/async-queue@^1.5.2", "@sapphire/async-queue@^1.5.3": 113 | version "1.5.3" 114 | resolved "https://registry.yarnpkg.com/@sapphire/async-queue/-/async-queue-1.5.3.tgz#03cd2a2f3665068f314736bdc56eee2025352422" 115 | integrity sha512-x7zadcfJGxFka1Q3f8gCts1F0xMwCKbZweM85xECGI0hBTeIZJGGCrHgLggihBoprlQ/hBmDR5LKfIPqnmHM3w== 116 | 117 | "@sapphire/shapeshift@^4.0.0": 118 | version "4.0.0" 119 | resolved "https://registry.yarnpkg.com/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz#86c1b41002ff5d0b2ad21cbc3418b06834b89040" 120 | integrity sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg== 121 | dependencies: 122 | fast-deep-equal "^3.1.3" 123 | lodash "^4.17.21" 124 | 125 | "@sapphire/snowflake@3.5.3", "@sapphire/snowflake@^3.5.3": 126 | version "3.5.3" 127 | resolved "https://registry.yarnpkg.com/@sapphire/snowflake/-/snowflake-3.5.3.tgz#0c102aa2ec5b34f806e9bc8625fc6a5e1d0a0c6a" 128 | integrity sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ== 129 | 130 | "@types/archiver@^6.0.3": 131 | version "6.0.3" 132 | resolved "https://registry.yarnpkg.com/@types/archiver/-/archiver-6.0.3.tgz#074eb6f4febc0128c25a205a8263da3d4688df53" 133 | integrity sha512-a6wUll6k3zX6qs5KlxIggs1P1JcYJaTCx2gnlr+f0S1yd2DoaEwoIK10HmBaLnZwWneBz+JBm0dwcZu0zECBcQ== 134 | dependencies: 135 | "@types/readdir-glob" "*" 136 | 137 | "@types/body-parser@*": 138 | version "1.19.5" 139 | resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" 140 | integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== 141 | dependencies: 142 | "@types/connect" "*" 143 | "@types/node" "*" 144 | 145 | "@types/connect@*": 146 | version "3.4.38" 147 | resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" 148 | integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== 149 | dependencies: 150 | "@types/node" "*" 151 | 152 | "@types/express-serve-static-core@^5.0.0": 153 | version "5.0.2" 154 | resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.0.2.tgz#812d2871e5eea17fb0bd5214dda7a7b748c0e12a" 155 | integrity sha512-vluaspfvWEtE4vcSDlKRNer52DvOGrB2xv6diXy6UKyKW0lqZiWHGNApSyxOv+8DE5Z27IzVvE7hNkxg7EXIcg== 156 | dependencies: 157 | "@types/node" "*" 158 | "@types/qs" "*" 159 | "@types/range-parser" "*" 160 | "@types/send" "*" 161 | 162 | "@types/express@^5.0.0": 163 | version "5.0.0" 164 | resolved "https://registry.yarnpkg.com/@types/express/-/express-5.0.0.tgz#13a7d1f75295e90d19ed6e74cab3678488eaa96c" 165 | integrity sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ== 166 | dependencies: 167 | "@types/body-parser" "*" 168 | "@types/express-serve-static-core" "^5.0.0" 169 | "@types/qs" "*" 170 | "@types/serve-static" "*" 171 | 172 | "@types/http-errors@*": 173 | version "2.0.4" 174 | resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" 175 | integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== 176 | 177 | "@types/mime-types@^2.1.1": 178 | version "2.1.4" 179 | resolved "https://registry.yarnpkg.com/@types/mime-types/-/mime-types-2.1.4.tgz#93a1933e24fed4fb9e4adc5963a63efcbb3317a2" 180 | integrity sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w== 181 | 182 | "@types/mime@^1": 183 | version "1.3.5" 184 | resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" 185 | integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== 186 | 187 | "@types/node@*": 188 | version "22.5.4" 189 | resolved "https://registry.yarnpkg.com/@types/node/-/node-22.5.4.tgz#83f7d1f65bc2ed223bdbf57c7884f1d5a4fa84e8" 190 | integrity sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg== 191 | dependencies: 192 | undici-types "~6.19.2" 193 | 194 | "@types/node@^18.16.1": 195 | version "18.19.50" 196 | resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.50.tgz#8652b34ee7c0e7e2004b3f08192281808d41bf5a" 197 | integrity sha512-xonK+NRrMBRtkL1hVCc3G+uXtjh1Al4opBLjqVmipe5ZAaBYWW6cNAiBVZ1BvmkBhep698rP3UM3aRAdSALuhg== 198 | dependencies: 199 | undici-types "~5.26.4" 200 | 201 | "@types/object-hash@^3.0.3": 202 | version "3.0.6" 203 | resolved "https://registry.yarnpkg.com/@types/object-hash/-/object-hash-3.0.6.tgz#25c052428199d374ef723b7b0ed44b5bfe1b3029" 204 | integrity sha512-fOBV8C1FIu2ELinoILQ+ApxcUKz4ngq+IWUYrxSGjXzzjUALijilampwkMgEtJ+h2njAW3pi853QpzNVCHB73w== 205 | 206 | "@types/qs@*": 207 | version "6.9.17" 208 | resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.17.tgz#fc560f60946d0aeff2f914eb41679659d3310e1a" 209 | integrity sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ== 210 | 211 | "@types/range-parser@*": 212 | version "1.2.7" 213 | resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" 214 | integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== 215 | 216 | "@types/readdir-glob@*": 217 | version "1.1.5" 218 | resolved "https://registry.yarnpkg.com/@types/readdir-glob/-/readdir-glob-1.1.5.tgz#21a4a98898fc606cb568ad815f2a0eedc24d412a" 219 | integrity sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg== 220 | dependencies: 221 | "@types/node" "*" 222 | 223 | "@types/send@*": 224 | version "0.17.4" 225 | resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a" 226 | integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== 227 | dependencies: 228 | "@types/mime" "^1" 229 | "@types/node" "*" 230 | 231 | "@types/serve-static@*": 232 | version "1.15.7" 233 | resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.7.tgz#22174bbd74fb97fe303109738e9b5c2f3064f714" 234 | integrity sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw== 235 | dependencies: 236 | "@types/http-errors" "*" 237 | "@types/node" "*" 238 | "@types/send" "*" 239 | 240 | "@types/ungap__structured-clone@^0.3.0": 241 | version "0.3.3" 242 | resolved "https://registry.yarnpkg.com/@types/ungap__structured-clone/-/ungap__structured-clone-0.3.3.tgz#cf7e1252f18f5ee39291a8f52fa83c31b0102fc6" 243 | integrity sha512-RNmhIPwoip6K/zZOv3ypksTAqaqLEXvlNSXKyrC93xMSOAHZCR7PifW6xKZCwkbbnbM9dwB9X56PPoNTlNwEqw== 244 | 245 | "@types/ws@^8.5.10": 246 | version "8.5.12" 247 | resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.12.tgz#619475fe98f35ccca2a2f6c137702d85ec247b7e" 248 | integrity sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ== 249 | dependencies: 250 | "@types/node" "*" 251 | 252 | "@ungap/structured-clone@^1.2.0": 253 | version "1.2.0" 254 | resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" 255 | integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== 256 | 257 | "@vladfrangu/async_event_emitter@^2.2.4", "@vladfrangu/async_event_emitter@^2.4.6": 258 | version "2.4.6" 259 | resolved "https://registry.yarnpkg.com/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.6.tgz#508b6c45b03f917112a9008180b308ba0e4d1805" 260 | integrity sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA== 261 | 262 | abort-controller@^3.0.0: 263 | version "3.0.0" 264 | resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" 265 | integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== 266 | dependencies: 267 | event-target-shim "^5.0.0" 268 | 269 | accepts@~1.3.8: 270 | version "1.3.8" 271 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" 272 | integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== 273 | dependencies: 274 | mime-types "~2.1.34" 275 | negotiator "0.6.3" 276 | 277 | ansi-regex@^5.0.1: 278 | version "5.0.1" 279 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" 280 | integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== 281 | 282 | ansi-regex@^6.0.1: 283 | version "6.1.0" 284 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.1.0.tgz#95ec409c69619d6cb1b8b34f14b660ef28ebd654" 285 | integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== 286 | 287 | ansi-styles@^4.0.0: 288 | version "4.3.0" 289 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" 290 | integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== 291 | dependencies: 292 | color-convert "^2.0.1" 293 | 294 | ansi-styles@^6.1.0: 295 | version "6.2.1" 296 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" 297 | integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== 298 | 299 | app-root-path@^3.1.0: 300 | version "3.1.0" 301 | resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-3.1.0.tgz#5971a2fc12ba170369a7a1ef018c71e6e47c2e86" 302 | integrity sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA== 303 | 304 | archiver-utils@^5.0.0, archiver-utils@^5.0.2: 305 | version "5.0.2" 306 | resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-5.0.2.tgz#63bc719d951803efc72cf961a56ef810760dd14d" 307 | integrity sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA== 308 | dependencies: 309 | glob "^10.0.0" 310 | graceful-fs "^4.2.0" 311 | is-stream "^2.0.1" 312 | lazystream "^1.0.0" 313 | lodash "^4.17.15" 314 | normalize-path "^3.0.0" 315 | readable-stream "^4.0.0" 316 | 317 | archiver@^7.0.1: 318 | version "7.0.1" 319 | resolved "https://registry.yarnpkg.com/archiver/-/archiver-7.0.1.tgz#c9d91c350362040b8927379c7aa69c0655122f61" 320 | integrity sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ== 321 | dependencies: 322 | archiver-utils "^5.0.2" 323 | async "^3.2.4" 324 | buffer-crc32 "^1.0.0" 325 | readable-stream "^4.0.0" 326 | readdir-glob "^1.1.2" 327 | tar-stream "^3.0.0" 328 | zip-stream "^6.0.1" 329 | 330 | array-flatten@1.1.1: 331 | version "1.1.1" 332 | resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" 333 | integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== 334 | 335 | async@^3.2.4: 336 | version "3.2.6" 337 | resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" 338 | integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== 339 | 340 | asynckit@^0.4.0: 341 | version "0.4.0" 342 | resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" 343 | integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== 344 | 345 | axios-retry@^4.5.0: 346 | version "4.5.0" 347 | resolved "https://registry.yarnpkg.com/axios-retry/-/axios-retry-4.5.0.tgz#441fdc32cedf63d6abd5de5d53db3667afd4c39b" 348 | integrity sha512-aR99oXhpEDGo0UuAlYcn2iGRds30k366Zfa05XWScR9QaQD4JYiP3/1Qt1u7YlefUOK+cn0CcwoL1oefavQUlQ== 349 | dependencies: 350 | is-retry-allowed "^2.2.0" 351 | 352 | axios@^1.3.6: 353 | version "1.7.7" 354 | resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f" 355 | integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q== 356 | dependencies: 357 | follow-redirects "^1.15.6" 358 | form-data "^4.0.0" 359 | proxy-from-env "^1.1.0" 360 | 361 | b4a@^1.6.4: 362 | version "1.6.7" 363 | resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.6.7.tgz#a99587d4ebbfbd5a6e3b21bdb5d5fa385767abe4" 364 | integrity sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg== 365 | 366 | balanced-match@^1.0.0: 367 | version "1.0.2" 368 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" 369 | integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== 370 | 371 | bare-events@^2.2.0: 372 | version "2.5.0" 373 | resolved "https://registry.yarnpkg.com/bare-events/-/bare-events-2.5.0.tgz#305b511e262ffd8b9d5616b056464f8e1b3329cc" 374 | integrity sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A== 375 | 376 | base64-js@^1.3.1: 377 | version "1.5.1" 378 | resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" 379 | integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== 380 | 381 | body-parser@1.20.3: 382 | version "1.20.3" 383 | resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" 384 | integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== 385 | dependencies: 386 | bytes "3.1.2" 387 | content-type "~1.0.5" 388 | debug "2.6.9" 389 | depd "2.0.0" 390 | destroy "1.2.0" 391 | http-errors "2.0.0" 392 | iconv-lite "0.4.24" 393 | on-finished "2.4.1" 394 | qs "6.13.0" 395 | raw-body "2.5.2" 396 | type-is "~1.6.18" 397 | unpipe "1.0.0" 398 | 399 | brace-expansion@^2.0.1: 400 | version "2.0.1" 401 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" 402 | integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== 403 | dependencies: 404 | balanced-match "^1.0.0" 405 | 406 | buffer-crc32@^1.0.0: 407 | version "1.0.0" 408 | resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-1.0.0.tgz#a10993b9055081d55304bd9feb4a072de179f405" 409 | integrity sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w== 410 | 411 | buffer@^6.0.3: 412 | version "6.0.3" 413 | resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" 414 | integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== 415 | dependencies: 416 | base64-js "^1.3.1" 417 | ieee754 "^1.2.1" 418 | 419 | bytes@3.1.2: 420 | version "3.1.2" 421 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" 422 | integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== 423 | 424 | call-bind-apply-helpers@^1.0.1: 425 | version "1.0.1" 426 | resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz#32e5892e6361b29b0b545ba6f7763378daca2840" 427 | integrity sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g== 428 | dependencies: 429 | es-errors "^1.3.0" 430 | function-bind "^1.1.2" 431 | 432 | call-bound@^1.0.2: 433 | version "1.0.3" 434 | resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.3.tgz#41cfd032b593e39176a71533ab4f384aa04fd681" 435 | integrity sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA== 436 | dependencies: 437 | call-bind-apply-helpers "^1.0.1" 438 | get-intrinsic "^1.2.6" 439 | 440 | color-convert@^2.0.1: 441 | version "2.0.1" 442 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" 443 | integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== 444 | dependencies: 445 | color-name "~1.1.4" 446 | 447 | color-loggers@^0.3.1: 448 | version "0.3.2" 449 | resolved "https://registry.yarnpkg.com/color-loggers/-/color-loggers-0.3.2.tgz#04b12224f4ef3f78c1bdfb238f2cee50f72d7e51" 450 | integrity sha512-asfXyY1/9N+Cxt30jb0PFy5tccybuMnWVc9J8EJuYoJVlcsUshn+pt2QuyUB3BWKMXVvEH8jgLrCFs11Am8QZA== 451 | 452 | color-name@~1.1.4: 453 | version "1.1.4" 454 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" 455 | integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== 456 | 457 | combined-stream@^1.0.8: 458 | version "1.0.8" 459 | resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" 460 | integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== 461 | dependencies: 462 | delayed-stream "~1.0.0" 463 | 464 | compress-commons@^6.0.2: 465 | version "6.0.2" 466 | resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-6.0.2.tgz#26d31251a66b9d6ba23a84064ecd3a6a71d2609e" 467 | integrity sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg== 468 | dependencies: 469 | crc-32 "^1.2.0" 470 | crc32-stream "^6.0.0" 471 | is-stream "^2.0.1" 472 | normalize-path "^3.0.0" 473 | readable-stream "^4.0.0" 474 | 475 | content-disposition@0.5.4: 476 | version "0.5.4" 477 | resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" 478 | integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== 479 | dependencies: 480 | safe-buffer "5.2.1" 481 | 482 | content-type@~1.0.4, content-type@~1.0.5: 483 | version "1.0.5" 484 | resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" 485 | integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== 486 | 487 | cookie-signature@1.0.6: 488 | version "1.0.6" 489 | resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" 490 | integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== 491 | 492 | cookie@0.7.1: 493 | version "0.7.1" 494 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.1.tgz#2f73c42142d5d5cf71310a74fc4ae61670e5dbc9" 495 | integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== 496 | 497 | core-util-is@~1.0.0: 498 | version "1.0.3" 499 | resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" 500 | integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== 501 | 502 | crc-32@^1.2.0: 503 | version "1.2.2" 504 | resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.2.tgz#3cad35a934b8bf71f25ca524b6da51fb7eace2ff" 505 | integrity sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ== 506 | 507 | crc32-stream@^6.0.0: 508 | version "6.0.0" 509 | resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-6.0.0.tgz#8529a3868f8b27abb915f6c3617c0fadedbf9430" 510 | integrity sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g== 511 | dependencies: 512 | crc-32 "^1.2.0" 513 | readable-stream "^4.0.0" 514 | 515 | cross-spawn@^7.0.0: 516 | version "7.0.6" 517 | resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" 518 | integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== 519 | dependencies: 520 | path-key "^3.1.0" 521 | shebang-command "^2.0.0" 522 | which "^2.0.1" 523 | 524 | debug@2.6.9: 525 | version "2.6.9" 526 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" 527 | integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== 528 | dependencies: 529 | ms "2.0.0" 530 | 531 | delayed-stream@~1.0.0: 532 | version "1.0.0" 533 | resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" 534 | integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== 535 | 536 | depd@2.0.0: 537 | version "2.0.0" 538 | resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" 539 | integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== 540 | 541 | dequal@^2.0.0: 542 | version "2.0.3" 543 | resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" 544 | integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== 545 | 546 | destroy@1.2.0: 547 | version "1.2.0" 548 | resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" 549 | integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== 550 | 551 | diff@^5.0.0: 552 | version "5.2.0" 553 | resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" 554 | integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A== 555 | 556 | discord-api-types@0.37.83: 557 | version "0.37.83" 558 | resolved "https://registry.yarnpkg.com/discord-api-types/-/discord-api-types-0.37.83.tgz#a22a799729ceded8176ea747157837ddf4708b1f" 559 | integrity sha512-urGGYeWtWNYMKnYlZnOnDHm8fVRffQs3U0SpE8RHeiuLKb/u92APS8HoQnPTFbnXmY1vVnXjXO4dOxcAn3J+DA== 560 | 561 | discord-api-types@0.37.97: 562 | version "0.37.97" 563 | resolved "https://registry.yarnpkg.com/discord-api-types/-/discord-api-types-0.37.97.tgz#d658573f726ad179261d538dbad4e7e8eca48d11" 564 | integrity sha512-No1BXPcVkyVD4ZVmbNgDKaBoqgeQ+FJpzZ8wqHkfmBnTZig1FcH3iPPersiK1TUIAzgClh2IvOuVUYfcWLQAOA== 565 | 566 | discord.js@^14.9.0: 567 | version "14.16.2" 568 | resolved "https://registry.yarnpkg.com/discord.js/-/discord.js-14.16.2.tgz#c878977b5a377cf41eaed0b1901115f8faec9852" 569 | integrity sha512-VGNi9WE2dZIxYM8/r/iatQQ+3LT8STW4hhczJOwm+DBeHq66vsKDCk8trChNCB01sMO9crslYuEMeZl2d7r3xw== 570 | dependencies: 571 | "@discordjs/builders" "^1.9.0" 572 | "@discordjs/collection" "1.5.3" 573 | "@discordjs/formatters" "^0.5.0" 574 | "@discordjs/rest" "^2.4.0" 575 | "@discordjs/util" "^1.1.1" 576 | "@discordjs/ws" "1.1.1" 577 | "@sapphire/snowflake" "3.5.3" 578 | discord-api-types "0.37.97" 579 | fast-deep-equal "3.1.3" 580 | lodash.snakecase "4.1.1" 581 | tslib "^2.6.3" 582 | undici "6.19.8" 583 | 584 | dotenv@^16.0.3: 585 | version "16.4.5" 586 | resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" 587 | integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== 588 | 589 | dunder-proto@^1.0.0: 590 | version "1.0.1" 591 | resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" 592 | integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== 593 | dependencies: 594 | call-bind-apply-helpers "^1.0.1" 595 | es-errors "^1.3.0" 596 | gopd "^1.2.0" 597 | 598 | eastasianwidth@^0.2.0: 599 | version "0.2.0" 600 | resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" 601 | integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== 602 | 603 | ee-first@1.1.1: 604 | version "1.1.1" 605 | resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" 606 | integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== 607 | 608 | emoji-regex@^8.0.0: 609 | version "8.0.0" 610 | resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" 611 | integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== 612 | 613 | emoji-regex@^9.2.2: 614 | version "9.2.2" 615 | resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" 616 | integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== 617 | 618 | encodeurl@~1.0.2: 619 | version "1.0.2" 620 | resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" 621 | integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== 622 | 623 | encodeurl@~2.0.0: 624 | version "2.0.0" 625 | resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" 626 | integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== 627 | 628 | es-define-property@^1.0.1: 629 | version "1.0.1" 630 | resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" 631 | integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== 632 | 633 | es-errors@^1.3.0: 634 | version "1.3.0" 635 | resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" 636 | integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== 637 | 638 | es-object-atoms@^1.0.0: 639 | version "1.0.0" 640 | resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" 641 | integrity sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw== 642 | dependencies: 643 | es-errors "^1.3.0" 644 | 645 | escape-html@~1.0.3: 646 | version "1.0.3" 647 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" 648 | integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== 649 | 650 | etag@~1.8.1: 651 | version "1.8.1" 652 | resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" 653 | integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== 654 | 655 | event-target-shim@^5.0.0: 656 | version "5.0.1" 657 | resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" 658 | integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== 659 | 660 | events@^3.3.0: 661 | version "3.3.0" 662 | resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" 663 | integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== 664 | 665 | express@^4.21.2: 666 | version "4.21.2" 667 | resolved "https://registry.yarnpkg.com/express/-/express-4.21.2.tgz#cf250e48362174ead6cea4a566abef0162c1ec32" 668 | integrity sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA== 669 | dependencies: 670 | accepts "~1.3.8" 671 | array-flatten "1.1.1" 672 | body-parser "1.20.3" 673 | content-disposition "0.5.4" 674 | content-type "~1.0.4" 675 | cookie "0.7.1" 676 | cookie-signature "1.0.6" 677 | debug "2.6.9" 678 | depd "2.0.0" 679 | encodeurl "~2.0.0" 680 | escape-html "~1.0.3" 681 | etag "~1.8.1" 682 | finalhandler "1.3.1" 683 | fresh "0.5.2" 684 | http-errors "2.0.0" 685 | merge-descriptors "1.0.3" 686 | methods "~1.1.2" 687 | on-finished "2.4.1" 688 | parseurl "~1.3.3" 689 | path-to-regexp "0.1.12" 690 | proxy-addr "~2.0.7" 691 | qs "6.13.0" 692 | range-parser "~1.2.1" 693 | safe-buffer "5.2.1" 694 | send "0.19.0" 695 | serve-static "1.16.2" 696 | setprototypeof "1.2.0" 697 | statuses "2.0.1" 698 | type-is "~1.6.18" 699 | utils-merge "1.0.1" 700 | vary "~1.1.2" 701 | 702 | fast-deep-equal@3.1.3, fast-deep-equal@^3.1.3: 703 | version "3.1.3" 704 | resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" 705 | integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== 706 | 707 | fast-fifo@^1.2.0, fast-fifo@^1.3.2: 708 | version "1.3.2" 709 | resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.2.tgz#286e31de96eb96d38a97899815740ba2a4f3640c" 710 | integrity sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ== 711 | 712 | finalhandler@1.3.1: 713 | version "1.3.1" 714 | resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019" 715 | integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ== 716 | dependencies: 717 | debug "2.6.9" 718 | encodeurl "~2.0.0" 719 | escape-html "~1.0.3" 720 | on-finished "2.4.1" 721 | parseurl "~1.3.3" 722 | statuses "2.0.1" 723 | unpipe "~1.0.0" 724 | 725 | follow-redirects@^1.15.6: 726 | version "1.15.9" 727 | resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" 728 | integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== 729 | 730 | foreground-child@^3.1.0: 731 | version "3.3.0" 732 | resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.0.tgz#0ac8644c06e431439f8561db8ecf29a7b5519c77" 733 | integrity sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg== 734 | dependencies: 735 | cross-spawn "^7.0.0" 736 | signal-exit "^4.0.1" 737 | 738 | form-data@^4.0.0: 739 | version "4.0.0" 740 | resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" 741 | integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== 742 | dependencies: 743 | asynckit "^0.4.0" 744 | combined-stream "^1.0.8" 745 | mime-types "^2.1.12" 746 | 747 | forwarded@0.2.0: 748 | version "0.2.0" 749 | resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" 750 | integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== 751 | 752 | fresh@0.5.2: 753 | version "0.5.2" 754 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" 755 | integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== 756 | 757 | function-bind@^1.1.2: 758 | version "1.1.2" 759 | resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" 760 | integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== 761 | 762 | get-intrinsic@^1.2.5, get-intrinsic@^1.2.6: 763 | version "1.2.6" 764 | resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.6.tgz#43dd3dd0e7b49b82b2dfcad10dc824bf7fc265d5" 765 | integrity sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA== 766 | dependencies: 767 | call-bind-apply-helpers "^1.0.1" 768 | dunder-proto "^1.0.0" 769 | es-define-property "^1.0.1" 770 | es-errors "^1.3.0" 771 | es-object-atoms "^1.0.0" 772 | function-bind "^1.1.2" 773 | gopd "^1.2.0" 774 | has-symbols "^1.1.0" 775 | hasown "^2.0.2" 776 | math-intrinsics "^1.0.0" 777 | 778 | glob@^10.0.0: 779 | version "10.4.5" 780 | resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" 781 | integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== 782 | dependencies: 783 | foreground-child "^3.1.0" 784 | jackspeak "^3.1.2" 785 | minimatch "^9.0.4" 786 | minipass "^7.1.2" 787 | package-json-from-dist "^1.0.0" 788 | path-scurry "^1.11.1" 789 | 790 | gopd@^1.2.0: 791 | version "1.2.0" 792 | resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" 793 | integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== 794 | 795 | graceful-fs@^4.2.0: 796 | version "4.2.11" 797 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" 798 | integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== 799 | 800 | has-symbols@^1.1.0: 801 | version "1.1.0" 802 | resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" 803 | integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== 804 | 805 | hasown@^2.0.2: 806 | version "2.0.2" 807 | resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" 808 | integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== 809 | dependencies: 810 | function-bind "^1.1.2" 811 | 812 | http-errors@2.0.0: 813 | version "2.0.0" 814 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" 815 | integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== 816 | dependencies: 817 | depd "2.0.0" 818 | inherits "2.0.4" 819 | setprototypeof "1.2.0" 820 | statuses "2.0.1" 821 | toidentifier "1.0.1" 822 | 823 | hyperdyperid@^1.2.0: 824 | version "1.2.0" 825 | resolved "https://registry.yarnpkg.com/hyperdyperid/-/hyperdyperid-1.2.0.tgz#59668d323ada92228d2a869d3e474d5a33b69e6b" 826 | integrity sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A== 827 | 828 | iconv-lite@0.4.24: 829 | version "0.4.24" 830 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" 831 | integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== 832 | dependencies: 833 | safer-buffer ">= 2.1.2 < 3" 834 | 835 | ieee754@^1.2.1: 836 | version "1.2.1" 837 | resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" 838 | integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== 839 | 840 | inherits@2.0.4, inherits@~2.0.3: 841 | version "2.0.4" 842 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" 843 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 844 | 845 | ipaddr.js@1.9.1: 846 | version "1.9.1" 847 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" 848 | integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== 849 | 850 | is-fullwidth-code-point@^3.0.0: 851 | version "3.0.0" 852 | resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" 853 | integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== 854 | 855 | is-retry-allowed@^2.2.0: 856 | version "2.2.0" 857 | resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz#88f34cbd236e043e71b6932d09b0c65fb7b4d71d" 858 | integrity sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg== 859 | 860 | is-stream@^2.0.1: 861 | version "2.0.1" 862 | resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" 863 | integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== 864 | 865 | isarray@~1.0.0: 866 | version "1.0.0" 867 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" 868 | integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== 869 | 870 | isexe@^2.0.0: 871 | version "2.0.0" 872 | resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" 873 | integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== 874 | 875 | jackspeak@^3.1.2: 876 | version "3.4.3" 877 | resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" 878 | integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== 879 | dependencies: 880 | "@isaacs/cliui" "^8.0.2" 881 | optionalDependencies: 882 | "@pkgjs/parseargs" "^0.11.0" 883 | 884 | kleur@^4.0.3: 885 | version "4.1.5" 886 | resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780" 887 | integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ== 888 | 889 | lazystream@^1.0.0: 890 | version "1.0.1" 891 | resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.1.tgz#494c831062f1f9408251ec44db1cba29242a2638" 892 | integrity sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw== 893 | dependencies: 894 | readable-stream "^2.0.5" 895 | 896 | lodash.snakecase@4.1.1: 897 | version "4.1.1" 898 | resolved "https://registry.yarnpkg.com/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz#39d714a35357147837aefd64b5dcbb16becd8f8d" 899 | integrity sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw== 900 | 901 | lodash@^4.17.15, lodash@^4.17.21: 902 | version "4.17.21" 903 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" 904 | integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== 905 | 906 | lru-cache@^10.2.0: 907 | version "10.4.3" 908 | resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" 909 | integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== 910 | 911 | magic-bytes.js@^1.10.0: 912 | version "1.10.0" 913 | resolved "https://registry.yarnpkg.com/magic-bytes.js/-/magic-bytes.js-1.10.0.tgz#c41cf4bc2f802992b05e64962411c9dd44fdef92" 914 | integrity sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ== 915 | 916 | math-intrinsics@^1.0.0: 917 | version "1.1.0" 918 | resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" 919 | integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== 920 | 921 | media-typer@0.3.0: 922 | version "0.3.0" 923 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" 924 | integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== 925 | 926 | memfs@^4.2.0: 927 | version "4.11.1" 928 | resolved "https://registry.yarnpkg.com/memfs/-/memfs-4.11.1.tgz#9c9c8e65bf8ac72c0db8d0fbbbe29248cf51d56a" 929 | integrity sha512-LZcMTBAgqUUKNXZagcZxvXXfgF1bHX7Y7nQ0QyEiNbRJgE29GhgPd8Yna1VQcLlPiHt/5RFJMWYN9Uv/VPNvjQ== 930 | dependencies: 931 | "@jsonjoy.com/json-pack" "^1.0.3" 932 | "@jsonjoy.com/util" "^1.3.0" 933 | tree-dump "^1.0.1" 934 | tslib "^2.0.0" 935 | 936 | merge-descriptors@1.0.3: 937 | version "1.0.3" 938 | resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" 939 | integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== 940 | 941 | methods@~1.1.2: 942 | version "1.1.2" 943 | resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" 944 | integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== 945 | 946 | mime-db@1.52.0: 947 | version "1.52.0" 948 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" 949 | integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== 950 | 951 | mime-db@^1.53.0: 952 | version "1.53.0" 953 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.53.0.tgz#3cb63cd820fc29896d9d4e8c32ab4fcd74ccb447" 954 | integrity sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg== 955 | 956 | mime-types@^2.1.12, mime-types@^2.1.18, mime-types@~2.1.24, mime-types@~2.1.34: 957 | version "2.1.35" 958 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" 959 | integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== 960 | dependencies: 961 | mime-db "1.52.0" 962 | 963 | mime-types@^3.0.0: 964 | version "3.0.0" 965 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-3.0.0.tgz#148453a900475522d095a445355c074cca4f5217" 966 | integrity sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w== 967 | dependencies: 968 | mime-db "^1.53.0" 969 | 970 | mime@1.6.0: 971 | version "1.6.0" 972 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" 973 | integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== 974 | 975 | minimatch@^5.1.0: 976 | version "5.1.6" 977 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" 978 | integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== 979 | dependencies: 980 | brace-expansion "^2.0.1" 981 | 982 | minimatch@^9.0.4: 983 | version "9.0.5" 984 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" 985 | integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== 986 | dependencies: 987 | brace-expansion "^2.0.1" 988 | 989 | "minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2: 990 | version "7.1.2" 991 | resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" 992 | integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== 993 | 994 | mri@^1.1.0: 995 | version "1.2.0" 996 | resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" 997 | integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== 998 | 999 | ms@2.0.0: 1000 | version "2.0.0" 1001 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 1002 | integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== 1003 | 1004 | ms@2.1.3: 1005 | version "2.1.3" 1006 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" 1007 | integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== 1008 | 1009 | negotiator@0.6.3: 1010 | version "0.6.3" 1011 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" 1012 | integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== 1013 | 1014 | normalize-path@^3.0.0: 1015 | version "3.0.0" 1016 | resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" 1017 | integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== 1018 | 1019 | object-hash@^3.0.0: 1020 | version "3.0.0" 1021 | resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" 1022 | integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== 1023 | 1024 | object-inspect@^1.13.3: 1025 | version "1.13.3" 1026 | resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.3.tgz#f14c183de51130243d6d18ae149375ff50ea488a" 1027 | integrity sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA== 1028 | 1029 | on-finished@2.4.1: 1030 | version "2.4.1" 1031 | resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" 1032 | integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== 1033 | dependencies: 1034 | ee-first "1.1.1" 1035 | 1036 | package-json-from-dist@^1.0.0: 1037 | version "1.0.1" 1038 | resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" 1039 | integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== 1040 | 1041 | parseurl@~1.3.3: 1042 | version "1.3.3" 1043 | resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" 1044 | integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== 1045 | 1046 | path-key@^3.1.0: 1047 | version "3.1.1" 1048 | resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" 1049 | integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== 1050 | 1051 | path-scurry@^1.11.1: 1052 | version "1.11.1" 1053 | resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" 1054 | integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== 1055 | dependencies: 1056 | lru-cache "^10.2.0" 1057 | minipass "^5.0.0 || ^6.0.2 || ^7.0.0" 1058 | 1059 | path-to-regexp@0.1.12: 1060 | version "0.1.12" 1061 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7" 1062 | integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ== 1063 | 1064 | process-nextick-args@~2.0.0: 1065 | version "2.0.1" 1066 | resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" 1067 | integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== 1068 | 1069 | process@^0.11.10: 1070 | version "0.11.10" 1071 | resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" 1072 | integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== 1073 | 1074 | proxy-addr@~2.0.7: 1075 | version "2.0.7" 1076 | resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" 1077 | integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== 1078 | dependencies: 1079 | forwarded "0.2.0" 1080 | ipaddr.js "1.9.1" 1081 | 1082 | proxy-from-env@^1.1.0: 1083 | version "1.1.0" 1084 | resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" 1085 | integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== 1086 | 1087 | qs@6.13.0: 1088 | version "6.13.0" 1089 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" 1090 | integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== 1091 | dependencies: 1092 | side-channel "^1.0.6" 1093 | 1094 | queue-tick@^1.0.1: 1095 | version "1.0.1" 1096 | resolved "https://registry.yarnpkg.com/queue-tick/-/queue-tick-1.0.1.tgz#f6f07ac82c1fd60f82e098b417a80e52f1f4c142" 1097 | integrity sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag== 1098 | 1099 | range-parser@~1.2.1: 1100 | version "1.2.1" 1101 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" 1102 | integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== 1103 | 1104 | raw-body@2.5.2: 1105 | version "2.5.2" 1106 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" 1107 | integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== 1108 | dependencies: 1109 | bytes "3.1.2" 1110 | http-errors "2.0.0" 1111 | iconv-lite "0.4.24" 1112 | unpipe "1.0.0" 1113 | 1114 | readable-stream@^2.0.5: 1115 | version "2.3.8" 1116 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" 1117 | integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== 1118 | dependencies: 1119 | core-util-is "~1.0.0" 1120 | inherits "~2.0.3" 1121 | isarray "~1.0.0" 1122 | process-nextick-args "~2.0.0" 1123 | safe-buffer "~5.1.1" 1124 | string_decoder "~1.1.1" 1125 | util-deprecate "~1.0.1" 1126 | 1127 | readable-stream@^4.0.0: 1128 | version "4.6.0" 1129 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.6.0.tgz#ce412dfb19c04efde1c5936d99c27f37a1ff94c9" 1130 | integrity sha512-cbAdYt0VcnpN2Bekq7PU+k363ZRsPwJoEEJOEtSJQlJXzwaxt3FIo/uL+KeDSGIjJqtkwyge4KQgD2S2kd+CQw== 1131 | dependencies: 1132 | abort-controller "^3.0.0" 1133 | buffer "^6.0.3" 1134 | events "^3.3.0" 1135 | process "^0.11.10" 1136 | string_decoder "^1.3.0" 1137 | 1138 | readdir-glob@^1.1.2: 1139 | version "1.1.3" 1140 | resolved "https://registry.yarnpkg.com/readdir-glob/-/readdir-glob-1.1.3.tgz#c3d831f51f5e7bfa62fa2ffbe4b508c640f09584" 1141 | integrity sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA== 1142 | dependencies: 1143 | minimatch "^5.1.0" 1144 | 1145 | sade@^1.7.3: 1146 | version "1.8.1" 1147 | resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701" 1148 | integrity sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A== 1149 | dependencies: 1150 | mri "^1.1.0" 1151 | 1152 | safe-buffer@5.2.1, safe-buffer@~5.2.0: 1153 | version "5.2.1" 1154 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" 1155 | integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== 1156 | 1157 | safe-buffer@~5.1.0, safe-buffer@~5.1.1: 1158 | version "5.1.2" 1159 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" 1160 | integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== 1161 | 1162 | "safer-buffer@>= 2.1.2 < 3": 1163 | version "2.1.2" 1164 | resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" 1165 | integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== 1166 | 1167 | sax@^1.2.4: 1168 | version "1.4.1" 1169 | resolved "https://registry.yarnpkg.com/sax/-/sax-1.4.1.tgz#44cc8988377f126304d3b3fc1010c733b929ef0f" 1170 | integrity sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg== 1171 | 1172 | send@0.19.0: 1173 | version "0.19.0" 1174 | resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" 1175 | integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== 1176 | dependencies: 1177 | debug "2.6.9" 1178 | depd "2.0.0" 1179 | destroy "1.2.0" 1180 | encodeurl "~1.0.2" 1181 | escape-html "~1.0.3" 1182 | etag "~1.8.1" 1183 | fresh "0.5.2" 1184 | http-errors "2.0.0" 1185 | mime "1.6.0" 1186 | ms "2.1.3" 1187 | on-finished "2.4.1" 1188 | range-parser "~1.2.1" 1189 | statuses "2.0.1" 1190 | 1191 | serve-static@1.16.2: 1192 | version "1.16.2" 1193 | resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296" 1194 | integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== 1195 | dependencies: 1196 | encodeurl "~2.0.0" 1197 | escape-html "~1.0.3" 1198 | parseurl "~1.3.3" 1199 | send "0.19.0" 1200 | 1201 | setprototypeof@1.2.0: 1202 | version "1.2.0" 1203 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" 1204 | integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== 1205 | 1206 | shebang-command@^2.0.0: 1207 | version "2.0.0" 1208 | resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" 1209 | integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== 1210 | dependencies: 1211 | shebang-regex "^3.0.0" 1212 | 1213 | shebang-regex@^3.0.0: 1214 | version "3.0.0" 1215 | resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" 1216 | integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== 1217 | 1218 | side-channel-list@^1.0.0: 1219 | version "1.0.0" 1220 | resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" 1221 | integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== 1222 | dependencies: 1223 | es-errors "^1.3.0" 1224 | object-inspect "^1.13.3" 1225 | 1226 | side-channel-map@^1.0.1: 1227 | version "1.0.1" 1228 | resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" 1229 | integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== 1230 | dependencies: 1231 | call-bound "^1.0.2" 1232 | es-errors "^1.3.0" 1233 | get-intrinsic "^1.2.5" 1234 | object-inspect "^1.13.3" 1235 | 1236 | side-channel-weakmap@^1.0.2: 1237 | version "1.0.2" 1238 | resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" 1239 | integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== 1240 | dependencies: 1241 | call-bound "^1.0.2" 1242 | es-errors "^1.3.0" 1243 | get-intrinsic "^1.2.5" 1244 | object-inspect "^1.13.3" 1245 | side-channel-map "^1.0.1" 1246 | 1247 | side-channel@^1.0.6: 1248 | version "1.1.0" 1249 | resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" 1250 | integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== 1251 | dependencies: 1252 | es-errors "^1.3.0" 1253 | object-inspect "^1.13.3" 1254 | side-channel-list "^1.0.0" 1255 | side-channel-map "^1.0.1" 1256 | side-channel-weakmap "^1.0.2" 1257 | 1258 | signal-exit@^4.0.1: 1259 | version "4.1.0" 1260 | resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" 1261 | integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== 1262 | 1263 | statuses@2.0.1: 1264 | version "2.0.1" 1265 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" 1266 | integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== 1267 | 1268 | streamx@^2.15.0: 1269 | version "2.21.1" 1270 | resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.21.1.tgz#f02979d8395b6b637d08a589fb514498bed55845" 1271 | integrity sha512-PhP9wUnFLa+91CPy3N6tiQsK+gnYyUNuk15S3YG/zjYE7RuPeCjJngqnzpC31ow0lzBHQ+QGO4cNJnd0djYUsw== 1272 | dependencies: 1273 | fast-fifo "^1.3.2" 1274 | queue-tick "^1.0.1" 1275 | text-decoder "^1.1.0" 1276 | optionalDependencies: 1277 | bare-events "^2.2.0" 1278 | 1279 | "string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0: 1280 | name string-width-cjs 1281 | version "4.2.3" 1282 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" 1283 | integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== 1284 | dependencies: 1285 | emoji-regex "^8.0.0" 1286 | is-fullwidth-code-point "^3.0.0" 1287 | strip-ansi "^6.0.1" 1288 | 1289 | string-width@^5.0.1, string-width@^5.1.2: 1290 | version "5.1.2" 1291 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" 1292 | integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== 1293 | dependencies: 1294 | eastasianwidth "^0.2.0" 1295 | emoji-regex "^9.2.2" 1296 | strip-ansi "^7.0.1" 1297 | 1298 | string_decoder@^1.3.0: 1299 | version "1.3.0" 1300 | resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" 1301 | integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== 1302 | dependencies: 1303 | safe-buffer "~5.2.0" 1304 | 1305 | string_decoder@~1.1.1: 1306 | version "1.1.1" 1307 | resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" 1308 | integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== 1309 | dependencies: 1310 | safe-buffer "~5.1.0" 1311 | 1312 | "strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: 1313 | name strip-ansi-cjs 1314 | version "6.0.1" 1315 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" 1316 | integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== 1317 | dependencies: 1318 | ansi-regex "^5.0.1" 1319 | 1320 | strip-ansi@^7.0.1: 1321 | version "7.1.0" 1322 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" 1323 | integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== 1324 | dependencies: 1325 | ansi-regex "^6.0.1" 1326 | 1327 | tar-stream@^3.0.0: 1328 | version "3.1.7" 1329 | resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-3.1.7.tgz#24b3fb5eabada19fe7338ed6d26e5f7c482e792b" 1330 | integrity sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ== 1331 | dependencies: 1332 | b4a "^1.6.4" 1333 | fast-fifo "^1.2.0" 1334 | streamx "^2.15.0" 1335 | 1336 | text-decoder@^1.1.0: 1337 | version "1.2.3" 1338 | resolved "https://registry.yarnpkg.com/text-decoder/-/text-decoder-1.2.3.tgz#b19da364d981b2326d5f43099c310cc80d770c65" 1339 | integrity sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA== 1340 | dependencies: 1341 | b4a "^1.6.4" 1342 | 1343 | thingies@^1.20.0: 1344 | version "1.21.0" 1345 | resolved "https://registry.yarnpkg.com/thingies/-/thingies-1.21.0.tgz#e80fbe58fd6fdaaab8fad9b67bd0a5c943c445c1" 1346 | integrity sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g== 1347 | 1348 | toidentifier@1.0.1: 1349 | version "1.0.1" 1350 | resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" 1351 | integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== 1352 | 1353 | tree-dump@^1.0.1: 1354 | version "1.0.2" 1355 | resolved "https://registry.yarnpkg.com/tree-dump/-/tree-dump-1.0.2.tgz#c460d5921caeb197bde71d0e9a7b479848c5b8ac" 1356 | integrity sha512-dpev9ABuLWdEubk+cIaI9cHwRNNDjkBBLXTwI4UCUFdQ5xXKqNXoK4FEciw/vxf+NQ7Cb7sGUyeUtORvHIdRXQ== 1357 | 1358 | ts-mixer@^6.0.4: 1359 | version "6.0.4" 1360 | resolved "https://registry.yarnpkg.com/ts-mixer/-/ts-mixer-6.0.4.tgz#1da39ceabc09d947a82140d9f09db0f84919ca28" 1361 | integrity sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA== 1362 | 1363 | tslib@^2.0.0, tslib@^2.6.2, tslib@^2.6.3: 1364 | version "2.7.0" 1365 | resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01" 1366 | integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== 1367 | 1368 | type-is@~1.6.18: 1369 | version "1.6.18" 1370 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" 1371 | integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== 1372 | dependencies: 1373 | media-typer "0.3.0" 1374 | mime-types "~2.1.24" 1375 | 1376 | typescript@^5.0.4: 1377 | version "5.6.2" 1378 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.2.tgz#d1de67b6bef77c41823f822df8f0b3bcff60a5a0" 1379 | integrity sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw== 1380 | 1381 | undici-types@~5.26.4: 1382 | version "5.26.5" 1383 | resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" 1384 | integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== 1385 | 1386 | undici-types@~6.19.2: 1387 | version "6.19.8" 1388 | resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" 1389 | integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== 1390 | 1391 | undici@6.19.8: 1392 | version "6.19.8" 1393 | resolved "https://registry.yarnpkg.com/undici/-/undici-6.19.8.tgz#002d7c8a28f8cc3a44ff33c3d4be4d85e15d40e1" 1394 | integrity sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g== 1395 | 1396 | unpipe@1.0.0, unpipe@~1.0.0: 1397 | version "1.0.0" 1398 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" 1399 | integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== 1400 | 1401 | util-deprecate@~1.0.1: 1402 | version "1.0.2" 1403 | resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" 1404 | integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== 1405 | 1406 | utils-merge@1.0.1: 1407 | version "1.0.1" 1408 | resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" 1409 | integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== 1410 | 1411 | uvu@^0.5.6: 1412 | version "0.5.6" 1413 | resolved "https://registry.yarnpkg.com/uvu/-/uvu-0.5.6.tgz#2754ca20bcb0bb59b64e9985e84d2e81058502df" 1414 | integrity sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA== 1415 | dependencies: 1416 | dequal "^2.0.0" 1417 | diff "^5.0.0" 1418 | kleur "^4.0.3" 1419 | sade "^1.7.3" 1420 | 1421 | vary@~1.1.2: 1422 | version "1.1.2" 1423 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" 1424 | integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== 1425 | 1426 | webdav-server@^2.6.2: 1427 | version "2.6.2" 1428 | resolved "https://registry.yarnpkg.com/webdav-server/-/webdav-server-2.6.2.tgz#5ea39b269aa34a1512c150e29c1b7e27d3a68908" 1429 | integrity sha512-0iHdrOzlKGFD96bTvPF8IIEfxw9Q7jB5LqWqhjyBYsofD6T6mOYqWtAvR88VY9Mq0xeg8bCRHC2Vifc9iuTYuw== 1430 | dependencies: 1431 | mime-types "^2.1.18" 1432 | xml-js-builder "^1.0.3" 1433 | 1434 | which@^2.0.1: 1435 | version "2.0.2" 1436 | resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" 1437 | integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== 1438 | dependencies: 1439 | isexe "^2.0.0" 1440 | 1441 | "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": 1442 | version "7.0.0" 1443 | resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" 1444 | integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== 1445 | dependencies: 1446 | ansi-styles "^4.0.0" 1447 | string-width "^4.1.0" 1448 | strip-ansi "^6.0.0" 1449 | 1450 | wrap-ansi@^8.1.0: 1451 | version "8.1.0" 1452 | resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" 1453 | integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== 1454 | dependencies: 1455 | ansi-styles "^6.1.0" 1456 | string-width "^5.0.1" 1457 | strip-ansi "^7.0.1" 1458 | 1459 | ws@^8.16.0: 1460 | version "8.18.0" 1461 | resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" 1462 | integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== 1463 | 1464 | xml-js-builder@^1.0.3: 1465 | version "1.0.3" 1466 | resolved "https://registry.yarnpkg.com/xml-js-builder/-/xml-js-builder-1.0.3.tgz#91d275cb926f9dc4167f029357a5b2875b5d3894" 1467 | integrity sha512-BoLgG/glT45M0jK5PGh9h+iGrQxa8jJk9ofR63GroRifl2tbGB3/yYiVY3wQWHrZgWWfl9+7fhEB/VoD9mWnSg== 1468 | dependencies: 1469 | xml-js "^1.6.2" 1470 | 1471 | xml-js@^1.6.2: 1472 | version "1.6.11" 1473 | resolved "https://registry.yarnpkg.com/xml-js/-/xml-js-1.6.11.tgz#927d2f6947f7f1c19a316dd8eea3614e8b18f8e9" 1474 | integrity sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g== 1475 | dependencies: 1476 | sax "^1.2.4" 1477 | 1478 | yarn-upgrade-all@^0.7.2: 1479 | version "0.7.4" 1480 | resolved "https://registry.yarnpkg.com/yarn-upgrade-all/-/yarn-upgrade-all-0.7.4.tgz#bb23ca31e2b7028ef973fe48c882b24288988ad6" 1481 | integrity sha512-poqeMyl5LD+xkw9YN322UHqctTf/N6FLpLsAVUllZqGcntEYkDuKL5r4p+zB67z5MaLs8F3qpPNlc9jNjXSABw== 1482 | dependencies: 1483 | color-loggers "^0.3.1" 1484 | 1485 | zip-stream@^6.0.1: 1486 | version "6.0.1" 1487 | resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-6.0.1.tgz#e141b930ed60ccaf5d7fa9c8260e0d1748a2bbfb" 1488 | integrity sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA== 1489 | dependencies: 1490 | archiver-utils "^5.0.0" 1491 | compress-commons "^6.0.2" 1492 | readable-stream "^4.0.0" 1493 | --------------------------------------------------------------------------------