diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..95e452b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +.vscode +.gitignore +.prettierrc +LICENSE +README.md +node_modules +.env +.data +out +dist +tsconfig.tsbuildinfo \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6901dae --- /dev/null +++ b/.env.example @@ -0,0 +1,23 @@ +PORT= +REQUEST_TIMEOUT= +TRUST_PROXY= +FORCE_SSL= + +DISCORD_TOKEN= + +MAX__DISCORD_FILES= +MAX__DISCORD_FILE_SIZE= +MAX__UPLOAD_ID_LENGTH= +TARGET__CHANNEL= + +ACCOUNTS__REGISTRATION_ENABLED= +ACCOUNTS__REQUIRED_FOR_UPLOAD= + +MAIL__HOST= +MAIL__PORT= +MAIL__SECURE= +MAIL__SEND_FROM= +MAIL__USER= +MAIL__PASS= + +JWT_SECRET= \ No newline at end of file diff --git a/.gitignore b/.gitignore index 7119cfa..5522d8e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ node_modules .env .data -out \ No newline at end of file +out +dist +tsconfig.tsbuildinfo \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..191f09d --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "useTabs": false, + "semi": false, + "trailingComma": "es5", + "tabWidth": 4 +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..fb466fd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +FROM node:21-alpine AS base +WORKDIR /usr/src/app + +FROM base AS install +RUN mkdir -p /tmp/dev +COPY package.json package-lock.json /tmp/dev/ +RUN cd /tmp/dev && npm install + +RUN mkdir -p /tmp/prod +COPY package.json package-lock.json /tmp/prod/ +RUN cd /tmp/prod && npm install --omit=dev + +FROM base AS build +COPY --from=install /tmp/dev/node_modules node_modules +COPY . . + +RUN npm run build + +FROM base AS app +COPY --from=install /tmp/prod/node_modules node_modules +COPY --from=build /usr/src/app/out out +COPY --from=build /usr/src/app/dist dist +COPY package.json . +COPY assets assets + +EXPOSE 3000 +ENTRYPOINT [ "node", "./out/server/index.js" ] \ No newline at end of file diff --git a/README.md b/README.md index af32c12..352b58e 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,6 @@ Invite your bot to a server, and create a new `config.json` in the project root: { "maxDiscordFiles": 20, "maxDiscordFileSize": 26214400, - "targetGuild": "1024080490677936248", "targetChannel": "1024080525993971913", "requestTimeout":120000, "maxUploadIdLength":30, @@ -72,3 +71,4 @@ Although we believe monofile is not against Discord's developer terms of service Code written by Etcetera is currently licensed under [Unlicense](./LICENSE). Icons under `/assets/icons` were created by Microsoft, and as such are licensed under [different terms](./assets/icons/README.md) (MIT). + diff --git a/assets/icons/pound.svg b/assets/icons/pound.svg index 415a3ba..43d5443 100644 --- a/assets/icons/pound.svg +++ b/assets/icons/pound.svg @@ -1 +1,7 @@ + \ No newline at end of file diff --git a/assets/moller.png b/assets/moller.png new file mode 100644 index 0000000..076cacf Binary files /dev/null and b/assets/moller.png differ diff --git a/config.json b/config.json index a7e9d16..ad1a786 100644 --- a/config.json +++ b/config.json @@ -1,11 +1,10 @@ { - "maxDiscordFiles": 20, - "maxDiscordFileSize": 26214400, - "targetGuild": "1024080490677936248", + "maxDiscordFiles": 1000, + "maxDiscordFileSize": 10485760, + "targetGuild": "906767804575928390", "targetChannel": "1024080525993971913", - "requestTimeout":120000, - "maxUploadIdLength":30, - + "requestTimeout": 3600000, + "maxUploadIdLength": 30, "accounts": { "registrationEnabled": true, "requiredForUpload": false @@ -21,7 +20,6 @@ "from": "mono@fyle.uk" } }, - "trustProxy": true, - "forceSSL": true -} + "forceSSL": false +} \ No newline at end of file diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..2b533e3 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,10 @@ +services: + monofile: + container_name: "monofile" + image: monofile + build: . + env_file: .env + volumes: + - ".data:/usr/src/app/.data" + ports: + - "3000:3000" diff --git a/package-lock.json b/package-lock.json index 01020d4..0173b84 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,2915 +1,1436 @@ -{ - "name": "monofile", - "version": "1.4.0-dev", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "monofile", - "version": "1.4.0-dev", - "license": "Unlicense", - "dependencies": { - "@types/body-parser": "^1.19.2", - "@types/express": "^4.17.14", - "@types/multer": "^1.4.7", - "@types/nodemailer": "^6.4.8", - "axios": "^0.27.2", - "body-parser": "^1.20.0", - "bytes": "^3.1.2", - "cookie-parser": "^1.4.6", - "discord.js": "^14.7.1", - "dotenv": "^16.0.2", - "express": "^4.18.1", - "multer": "^1.4.5-lts.1", - "nodemailer": "^6.9.3", - "typescript": "^4.8.3" - }, - "devDependencies": { - "@rollup/plugin-node-resolve": "^15.0.1", - "@types/bytes": "^3.1.1", - "@types/cookie-parser": "^1.4.3", - "rollup": "^3.11.0", - "rollup-plugin-svelte": "^7.1.0", - "sass": "^1.57.1", - "svelte": "^3.55.1" - }, - "engines": { - "node": ">=v16.11" - } - }, - "node_modules/@discordjs/builders": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.4.0.tgz", - "integrity": "sha512-nEeTCheTTDw5kO93faM1j8ZJPonAX86qpq/QVoznnSa8WWcCgJpjlu6GylfINTDW6o7zZY0my2SYdxx2mfNwGA==", - "dependencies": { - "@discordjs/util": "^0.1.0", - "@sapphire/shapeshift": "^3.7.1", - "discord-api-types": "^0.37.20", - "fast-deep-equal": "^3.1.3", - "ts-mixer": "^6.0.2", - "tslib": "^2.4.1" - }, - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/@discordjs/collection": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.3.0.tgz", - "integrity": "sha512-ylt2NyZ77bJbRij4h9u/wVy7qYw/aDqQLWnadjvDqW/WoWCxrsX6M3CIw9GVP5xcGCDxsrKj5e0r5evuFYwrKg==", - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/@discordjs/rest": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-1.5.0.tgz", - "integrity": "sha512-lXgNFqHnbmzp5u81W0+frdXN6Etf4EUi8FAPcWpSykKd8hmlWh1xy6BmE0bsJypU1pxohaA8lQCgp70NUI3uzA==", - "dependencies": { - "@discordjs/collection": "^1.3.0", - "@discordjs/util": "^0.1.0", - "@sapphire/async-queue": "^1.5.0", - "@sapphire/snowflake": "^3.2.2", - "discord-api-types": "^0.37.23", - "file-type": "^18.0.0", - "tslib": "^2.4.1", - "undici": "^5.13.0" - }, - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/@discordjs/util": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-0.1.0.tgz", - "integrity": "sha512-e7d+PaTLVQav6rOc2tojh2y6FE8S7REkqLldq1XF4soCx74XB/DIjbVbVLtBemf0nLW77ntz0v+o5DytKwFNLQ==", - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/@rollup/plugin-node-resolve": { - "version": "15.0.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.0.1.tgz", - "integrity": "sha512-ReY88T7JhJjeRVbfCyNj+NXAG3IIsVMsX9b5/9jC98dRP8/yxlZdz7mHZbHk5zHr24wZZICS5AcXsFZAXYUQEg==", - "dev": true, - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "@types/resolve": "1.20.2", - "deepmerge": "^4.2.2", - "is-builtin-module": "^3.2.0", - "is-module": "^1.0.0", - "resolve": "^1.22.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.78.0||^3.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/pluginutils": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz", - "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==", - "dev": true, - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/pluginutils/node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true - }, - "node_modules/@sapphire/async-queue": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.0.tgz", - "integrity": "sha512-JkLdIsP8fPAdh9ZZjrbHWR/+mZj0wvKS5ICibcLrRI1j84UmLMshx5n9QmL8b95d4onJ2xxiyugTgSAX7AalmA==", - "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/@sapphire/shapeshift": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-3.8.1.tgz", - "integrity": "sha512-xG1oXXBhCjPKbxrRTlox9ddaZTvVpOhYLmKmApD/vIWOV1xEYXnpoFs68zHIZBGbqztq6FrUPNPerIrO1Hqeaw==", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "lodash": "^4.17.21" - }, - "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/@sapphire/snowflake": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.4.0.tgz", - "integrity": "sha512-zZxymtVO6zeXVMPds+6d7gv/OfnCc25M1Z+7ZLB0oPmeMTPeRWVPQSS16oDJy5ZsyCOLj7M6mbZml5gWXcVRNw==", - "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/@tokenizer/token": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", - "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" - }, - "node_modules/@types/body-parser": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", - "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/bytes": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@types/bytes/-/bytes-3.1.1.tgz", - "integrity": "sha512-lOGyCnw+2JVPKU3wIV0srU0NyALwTBJlVSx5DfMQOFuuohA8y9S8orImpuIQikZ0uIQ8gehrRjxgQC1rLRi11w==", - "dev": true - }, - "node_modules/@types/connect": { - "version": "3.4.35", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", - "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/cookie-parser": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.3.tgz", - "integrity": "sha512-CqSKwFwefj4PzZ5n/iwad/bow2hTCh0FlNAeWLtQM3JA/NX/iYagIpWG2cf1bQKQ2c9gU2log5VUCrn7LDOs0w==", - "dev": true, - "dependencies": { - "@types/express": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz", - "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==", - "dev": true - }, - "node_modules/@types/express": { - "version": "4.17.14", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.14.tgz", - "integrity": "sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg==", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.18", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "4.17.31", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz", - "integrity": "sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*" - } - }, - "node_modules/@types/mime": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", - "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==" - }, - "node_modules/@types/multer": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.7.tgz", - "integrity": "sha512-/SNsDidUFCvqqcWDwxv2feww/yqhNeTRL5CVoL3jU4Goc4kKEL10T7Eye65ZqPNi4HRx8sAEX59pV1aEH7drNA==", - "dependencies": { - "@types/express": "*" - } - }, - "node_modules/@types/node": { - "version": "18.7.23", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.23.tgz", - "integrity": "sha512-DWNcCHolDq0ZKGizjx2DZjR/PqsYwAcYUJmfMWqtVU2MBMG5Mo+xFZrhGId5r/O5HOuMPyQEcM6KUBp5lBZZBg==" - }, - "node_modules/@types/nodemailer": { - "version": "6.4.8", - "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.8.tgz", - "integrity": "sha512-oVsJSCkqViCn8/pEu2hfjwVO+Gb3e+eTWjg3PcjeFKRItfKpKwHphQqbYmPQrlMk+op7pNNWPbsJIEthpFN/OQ==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/qs": { - "version": "6.9.7", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" - }, - "node_modules/@types/range-parser": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", - "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" - }, - "node_modules/@types/resolve": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", - "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", - "dev": true - }, - "node_modules/@types/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==", - "dependencies": { - "@types/mime": "*", - "@types/node": "*" - } - }, - "node_modules/@types/ws": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", - "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/append-field": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", - "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/axios": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", - "dependencies": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" - } - }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/body-parser": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", - "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.10.3", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" - }, - "node_modules/builtin-modules": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", - "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", - "dev": true, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "dependencies": { - "streamsearch": "^1.1.0" - }, - "engines": { - "node": ">=10.16.0" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "engines": [ - "node >= 0.8" - ], - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-parser": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", - "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", - "dependencies": { - "cookie": "0.4.1", - "cookie-signature": "1.0.6" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/cookie-parser/node_modules/cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/deepmerge": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", - "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/discord-api-types": { - "version": "0.37.25", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.25.tgz", - "integrity": "sha512-aCwA2sWnL1zPQgTELkkMzQneuWyCXXUjZCUKswesiE6RDCfOfxAPXOHg6ZTlBA5layPSikGCBBRjyh8S3Wzd+A==" - }, - "node_modules/discord.js": { - "version": "14.7.1", - "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.7.1.tgz", - "integrity": "sha512-1FECvqJJjjeYcjSm0IGMnPxLqja/pmG1B0W2l3lUY2Gi4KXiyTeQmU1IxWcbXHn2k+ytP587mMWqva2IA87EbA==", - "dependencies": { - "@discordjs/builders": "^1.4.0", - "@discordjs/collection": "^1.3.0", - "@discordjs/rest": "^1.4.0", - "@discordjs/util": "^0.1.0", - "@sapphire/snowflake": "^3.2.2", - "@types/ws": "^8.5.3", - "discord-api-types": "^0.37.20", - "fast-deep-equal": "^3.1.3", - "lodash.snakecase": "^4.1.1", - "tslib": "^2.4.1", - "undici": "^5.13.0", - "ws": "^8.11.0" - }, - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/dotenv": { - "version": "16.0.2", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.2.tgz", - "integrity": "sha512-JvpYKUmzQhYoIFgK2MOnF3bciIZoItIIoryihy0rIA+H4Jy0FmgyKYAHCTN98P5ybGSJcIFbh6QKeJdtZd1qhA==", - "engines": { - "node": ">=12" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "node_modules/estree-walker": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", - "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", - "dev": true - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz", - "integrity": "sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.0", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.5.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.10.3", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "node_modules/file-type": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-18.0.0.tgz", - "integrity": "sha512-jjMwFpnW8PKofLE/4ohlhqwDk5k0NC6iy0UHAJFKoY1fQeGMN0GDdLgHQrvCbSpMwbqzoCZhRI5dETCZna5qVA==", - "dependencies": { - "readable-web-to-node-stream": "^3.0.2", - "strtok3": "^7.0.0", - "token-types": "^5.0.1" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/file-type?sponsor=1" - } - }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "node_modules/get-intrinsic": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", - "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/immutable": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.2.2.tgz", - "integrity": "sha512-fTMKDwtbvO5tldky9QZ2fMX7slR0mYpY5nbnFWYp0fOzDhHqhgIw9KoYgxLWsoNTS9ZHGauHj18DTyEw6BK3Og==", - "dev": true - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-builtin-module": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.0.tgz", - "integrity": "sha512-phDA4oSGt7vl1n5tJvTWooWWAsXLY+2xCnxNqvKhGEzujg+A43wPlPOyDg3C8XQHN+6k/JTQWJ/j0dQh/qr+Hw==", - "dev": true, - "dependencies": { - "builtin-modules": "^3.3.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", - "dev": true, - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", - "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", - "dev": true - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "node_modules/lodash.snakecase": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", - "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==" - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/multer": { - "version": "1.4.5-lts.1", - "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", - "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", - "dependencies": { - "append-field": "^1.0.0", - "busboy": "^1.0.0", - "concat-stream": "^1.5.2", - "mkdirp": "^0.5.4", - "object-assign": "^4.1.1", - "type-is": "^1.6.4", - "xtend": "^4.0.0" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/nodemailer": { - "version": "6.9.3", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.3.tgz", - "integrity": "sha512-fy9v3NgTzBngrMFkDsKEj0r02U7jm6XfC3b52eoNV+GCrGj+s8pt5OqhiJdWKuw51zCTdiNR/IUD1z33LIIGpg==", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", - "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" - }, - "node_modules/peek-readable": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0.tgz", - "integrity": "sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A==", - "engines": { - "node": ">=14.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/qs": { - "version": "6.10.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", - "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/readable-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/readable-web-to-node-stream": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz", - "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", - "dependencies": { - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/readable-web-to-node-stream/node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/require-relative": { - "version": "0.8.7", - "resolved": "https://registry.npmjs.org/require-relative/-/require-relative-0.8.7.tgz", - "integrity": "sha512-AKGr4qvHiryxRb19m3PsLRGuKVAbJLUD7E6eOaHkfKhwc+vSgVOCY5xNvm9EkolBKTOf0GrQAZKLimOCz81Khg==", - "dev": true - }, - "node_modules/resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", - "dev": true, - "dependencies": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/rollup": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.11.0.tgz", - "integrity": "sha512-+uWPPkpWQ2H3Qi7sNBcRfhhHJyUNgBYhG4wKe5wuGRj2m55kpo+0p5jubKNBjQODyPe6tSBE3tNpdDwEisQvAQ==", - "dev": true, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=14.18.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/rollup-plugin-svelte": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-svelte/-/rollup-plugin-svelte-7.1.0.tgz", - "integrity": "sha512-vopCUq3G+25sKjwF5VilIbiY6KCuMNHP1PFvx2Vr3REBNMDllKHFZN2B9jwwC+MqNc3UPKkjXnceLPEjTjXGXg==", - "dev": true, - "dependencies": { - "require-relative": "^0.8.7", - "rollup-pluginutils": "^2.8.2" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "rollup": ">=2.0.0", - "svelte": ">=3.5.0" - } - }, - "node_modules/rollup-pluginutils": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", - "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", - "dev": true, - "dependencies": { - "estree-walker": "^0.6.1" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/sass": { - "version": "1.57.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.57.1.tgz", - "integrity": "sha512-O2+LwLS79op7GI0xZ8fqzF7X2m/m8WFfI02dHOdsK5R2ECeS5F62zrwg/relM1rjSLy7Vd/DiMNIvPrQGsA0jw==", - "dev": true, - "dependencies": { - "chokidar": ">=3.0.0 <4.0.0", - "immutable": "^4.0.0", - "source-map-js": ">=0.6.2 <2.0.0" - }, - "bin": { - "sass": "sass.js" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "dependencies": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/strtok3": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-7.0.0.tgz", - "integrity": "sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ==", - "dependencies": { - "@tokenizer/token": "^0.3.0", - "peek-readable": "^5.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/svelte": { - "version": "3.55.1", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.55.1.tgz", - "integrity": "sha512-S+87/P0Ve67HxKkEV23iCdAh/SX1xiSfjF1HOglno/YTbSTW7RniICMCofWGdJJbdjw3S+0PfFb1JtGfTXE0oQ==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/token-types": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/token-types/-/token-types-5.0.1.tgz", - "integrity": "sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg==", - "dependencies": { - "@tokenizer/token": "^0.3.0", - "ieee754": "^1.2.1" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/ts-mixer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.2.tgz", - "integrity": "sha512-zvHx3VM83m2WYCE8XL99uaM7mFwYSkjR2OZti98fabHrwkjsCvgwChda5xctein3xGOyaQhtTeDq/1H/GNvF3A==" - }, - "node_modules/tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" - }, - "node_modules/typescript": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz", - "integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, - "node_modules/undici": { - "version": "5.14.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.14.0.tgz", - "integrity": "sha512-yJlHYw6yXPPsuOH0x2Ib1Km61vu4hLiRRQoafs+WUgX1vO64vgnxiCEN9dpIrhZyHFsai3F0AEj4P9zy19enEQ==", - "dependencies": { - "busboy": "^1.6.0" - }, - "engines": { - "node": ">=12.18" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "engines": { - "node": ">=0.4" - } - } - }, - "dependencies": { - "@discordjs/builders": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.4.0.tgz", - "integrity": "sha512-nEeTCheTTDw5kO93faM1j8ZJPonAX86qpq/QVoznnSa8WWcCgJpjlu6GylfINTDW6o7zZY0my2SYdxx2mfNwGA==", - "requires": { - "@discordjs/util": "^0.1.0", - "@sapphire/shapeshift": "^3.7.1", - "discord-api-types": "^0.37.20", - "fast-deep-equal": "^3.1.3", - "ts-mixer": "^6.0.2", - "tslib": "^2.4.1" - } - }, - "@discordjs/collection": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.3.0.tgz", - "integrity": "sha512-ylt2NyZ77bJbRij4h9u/wVy7qYw/aDqQLWnadjvDqW/WoWCxrsX6M3CIw9GVP5xcGCDxsrKj5e0r5evuFYwrKg==" - }, - "@discordjs/rest": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-1.5.0.tgz", - "integrity": "sha512-lXgNFqHnbmzp5u81W0+frdXN6Etf4EUi8FAPcWpSykKd8hmlWh1xy6BmE0bsJypU1pxohaA8lQCgp70NUI3uzA==", - "requires": { - "@discordjs/collection": "^1.3.0", - "@discordjs/util": "^0.1.0", - "@sapphire/async-queue": "^1.5.0", - "@sapphire/snowflake": "^3.2.2", - "discord-api-types": "^0.37.23", - "file-type": "^18.0.0", - "tslib": "^2.4.1", - "undici": "^5.13.0" - } - }, - "@discordjs/util": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-0.1.0.tgz", - "integrity": "sha512-e7d+PaTLVQav6rOc2tojh2y6FE8S7REkqLldq1XF4soCx74XB/DIjbVbVLtBemf0nLW77ntz0v+o5DytKwFNLQ==" - }, - "@rollup/plugin-node-resolve": { - "version": "15.0.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.0.1.tgz", - "integrity": "sha512-ReY88T7JhJjeRVbfCyNj+NXAG3IIsVMsX9b5/9jC98dRP8/yxlZdz7mHZbHk5zHr24wZZICS5AcXsFZAXYUQEg==", - "dev": true, - "requires": { - "@rollup/pluginutils": "^5.0.1", - "@types/resolve": "1.20.2", - "deepmerge": "^4.2.2", - "is-builtin-module": "^3.2.0", - "is-module": "^1.0.0", - "resolve": "^1.22.1" - } - }, - "@rollup/pluginutils": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz", - "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==", - "dev": true, - "requires": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^2.3.1" - }, - "dependencies": { - "estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true - } - } - }, - "@sapphire/async-queue": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.0.tgz", - "integrity": "sha512-JkLdIsP8fPAdh9ZZjrbHWR/+mZj0wvKS5ICibcLrRI1j84UmLMshx5n9QmL8b95d4onJ2xxiyugTgSAX7AalmA==" - }, - "@sapphire/shapeshift": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-3.8.1.tgz", - "integrity": "sha512-xG1oXXBhCjPKbxrRTlox9ddaZTvVpOhYLmKmApD/vIWOV1xEYXnpoFs68zHIZBGbqztq6FrUPNPerIrO1Hqeaw==", - "requires": { - "fast-deep-equal": "^3.1.3", - "lodash": "^4.17.21" - } - }, - "@sapphire/snowflake": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.4.0.tgz", - "integrity": "sha512-zZxymtVO6zeXVMPds+6d7gv/OfnCc25M1Z+7ZLB0oPmeMTPeRWVPQSS16oDJy5ZsyCOLj7M6mbZml5gWXcVRNw==" - }, - "@tokenizer/token": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", - "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" - }, - "@types/body-parser": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", - "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", - "requires": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "@types/bytes": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@types/bytes/-/bytes-3.1.1.tgz", - "integrity": "sha512-lOGyCnw+2JVPKU3wIV0srU0NyALwTBJlVSx5DfMQOFuuohA8y9S8orImpuIQikZ0uIQ8gehrRjxgQC1rLRi11w==", - "dev": true - }, - "@types/connect": { - "version": "3.4.35", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", - "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", - "requires": { - "@types/node": "*" - } - }, - "@types/cookie-parser": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.3.tgz", - "integrity": "sha512-CqSKwFwefj4PzZ5n/iwad/bow2hTCh0FlNAeWLtQM3JA/NX/iYagIpWG2cf1bQKQ2c9gU2log5VUCrn7LDOs0w==", - "dev": true, - "requires": { - "@types/express": "*" - } - }, - "@types/estree": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz", - "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==", - "dev": true - }, - "@types/express": { - "version": "4.17.14", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.14.tgz", - "integrity": "sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg==", - "requires": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.18", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "@types/express-serve-static-core": { - "version": "4.17.31", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz", - "integrity": "sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==", - "requires": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*" - } - }, - "@types/mime": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", - "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==" - }, - "@types/multer": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.7.tgz", - "integrity": "sha512-/SNsDidUFCvqqcWDwxv2feww/yqhNeTRL5CVoL3jU4Goc4kKEL10T7Eye65ZqPNi4HRx8sAEX59pV1aEH7drNA==", - "requires": { - "@types/express": "*" - } - }, - "@types/node": { - "version": "18.7.23", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.23.tgz", - "integrity": "sha512-DWNcCHolDq0ZKGizjx2DZjR/PqsYwAcYUJmfMWqtVU2MBMG5Mo+xFZrhGId5r/O5HOuMPyQEcM6KUBp5lBZZBg==" - }, - "@types/nodemailer": { - "version": "6.4.8", - "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.8.tgz", - "integrity": "sha512-oVsJSCkqViCn8/pEu2hfjwVO+Gb3e+eTWjg3PcjeFKRItfKpKwHphQqbYmPQrlMk+op7pNNWPbsJIEthpFN/OQ==", - "requires": { - "@types/node": "*" - } - }, - "@types/qs": { - "version": "6.9.7", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" - }, - "@types/range-parser": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", - "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" - }, - "@types/resolve": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", - "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", - "dev": true - }, - "@types/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==", - "requires": { - "@types/mime": "*", - "@types/node": "*" - } - }, - "@types/ws": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", - "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==", - "requires": { - "@types/node": "*" - } - }, - "accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "requires": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - } - }, - "anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "append-field": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", - "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" - }, - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "axios": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", - "requires": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" - } - }, - "binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true - }, - "body-parser": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", - "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==", - "requires": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.10.3", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" - }, - "builtin-modules": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", - "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", - "dev": true - }, - "busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "requires": { - "streamsearch": "^1.1.0" - } - }, - "bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" - }, - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - }, - "chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "requires": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "fsevents": "~2.3.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - } - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "requires": { - "safe-buffer": "5.2.1" - } - }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" - }, - "cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" - }, - "cookie-parser": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", - "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", - "requires": { - "cookie": "0.4.1", - "cookie-signature": "1.0.6" - }, - "dependencies": { - "cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" - } - } - }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" - }, - "core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "deepmerge": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", - "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", - "dev": true - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" - }, - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" - }, - "destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" - }, - "discord-api-types": { - "version": "0.37.25", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.25.tgz", - "integrity": "sha512-aCwA2sWnL1zPQgTELkkMzQneuWyCXXUjZCUKswesiE6RDCfOfxAPXOHg6ZTlBA5layPSikGCBBRjyh8S3Wzd+A==" - }, - "discord.js": { - "version": "14.7.1", - "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.7.1.tgz", - "integrity": "sha512-1FECvqJJjjeYcjSm0IGMnPxLqja/pmG1B0W2l3lUY2Gi4KXiyTeQmU1IxWcbXHn2k+ytP587mMWqva2IA87EbA==", - "requires": { - "@discordjs/builders": "^1.4.0", - "@discordjs/collection": "^1.3.0", - "@discordjs/rest": "^1.4.0", - "@discordjs/util": "^0.1.0", - "@sapphire/snowflake": "^3.2.2", - "@types/ws": "^8.5.3", - "discord-api-types": "^0.37.20", - "fast-deep-equal": "^3.1.3", - "lodash.snakecase": "^4.1.1", - "tslib": "^2.4.1", - "undici": "^5.13.0", - "ws": "^8.11.0" - } - }, - "dotenv": { - "version": "16.0.2", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.2.tgz", - "integrity": "sha512-JvpYKUmzQhYoIFgK2MOnF3bciIZoItIIoryihy0rIA+H4Jy0FmgyKYAHCTN98P5ybGSJcIFbh6QKeJdtZd1qhA==" - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "estree-walker": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", - "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", - "dev": true - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" - }, - "express": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz", - "integrity": "sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==", - "requires": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.0", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.5.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.10.3", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - } - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "file-type": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-18.0.0.tgz", - "integrity": "sha512-jjMwFpnW8PKofLE/4ohlhqwDk5k0NC6iy0UHAJFKoY1fQeGMN0GDdLgHQrvCbSpMwbqzoCZhRI5dETCZna5qVA==", - "requires": { - "readable-web-to-node-stream": "^3.0.2", - "strtok3": "^7.0.0", - "token-types": "^5.0.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - } - }, - "follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" - }, - "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, - "forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" - }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "optional": true - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "get-intrinsic": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", - "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" - } - }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" - }, - "http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "requires": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - } - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" - }, - "immutable": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.2.2.tgz", - "integrity": "sha512-fTMKDwtbvO5tldky9QZ2fMX7slR0mYpY5nbnFWYp0fOzDhHqhgIw9KoYgxLWsoNTS9ZHGauHj18DTyEw6BK3Og==", - "dev": true - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-builtin-module": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.0.tgz", - "integrity": "sha512-phDA4oSGt7vl1n5tJvTWooWWAsXLY+2xCnxNqvKhGEzujg+A43wPlPOyDg3C8XQHN+6k/JTQWJ/j0dQh/qr+Hw==", - "dev": true, - "requires": { - "builtin-modules": "^3.3.0" - } - }, - "is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", - "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", - "dev": true - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "lodash.snakecase": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", - "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==" - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" - }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" - }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" - }, - "mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" - }, - "mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "requires": { - "mime-db": "1.52.0" - } - }, - "minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" - }, - "mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "requires": { - "minimist": "^1.2.6" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "multer": { - "version": "1.4.5-lts.1", - "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", - "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", - "requires": { - "append-field": "^1.0.0", - "busboy": "^1.0.0", - "concat-stream": "^1.5.2", - "mkdirp": "^0.5.4", - "object-assign": "^4.1.1", - "type-is": "^1.6.4", - "xtend": "^4.0.0" - } - }, - "negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" - }, - "nodemailer": { - "version": "6.9.3", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.3.tgz", - "integrity": "sha512-fy9v3NgTzBngrMFkDsKEj0r02U7jm6XfC3b52eoNV+GCrGj+s8pt5OqhiJdWKuw51zCTdiNR/IUD1z33LIIGpg==" - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" - }, - "object-inspect": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", - "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" - }, - "on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "requires": { - "ee-first": "1.1.1" - } - }, - "parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" - }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" - }, - "peek-readable": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0.tgz", - "integrity": "sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A==" - }, - "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "requires": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - } - }, - "qs": { - "version": "6.10.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", - "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", - "requires": { - "side-channel": "^1.0.4" - } - }, - "range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" - }, - "raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "requires": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - } - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - } - } - }, - "readable-web-to-node-stream": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz", - "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", - "requires": { - "readable-stream": "^3.6.0" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } - } - }, - "readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "requires": { - "picomatch": "^2.2.1" - } - }, - "require-relative": { - "version": "0.8.7", - "resolved": "https://registry.npmjs.org/require-relative/-/require-relative-0.8.7.tgz", - "integrity": "sha512-AKGr4qvHiryxRb19m3PsLRGuKVAbJLUD7E6eOaHkfKhwc+vSgVOCY5xNvm9EkolBKTOf0GrQAZKLimOCz81Khg==", - "dev": true - }, - "resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", - "dev": true, - "requires": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - }, - "rollup": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.11.0.tgz", - "integrity": "sha512-+uWPPkpWQ2H3Qi7sNBcRfhhHJyUNgBYhG4wKe5wuGRj2m55kpo+0p5jubKNBjQODyPe6tSBE3tNpdDwEisQvAQ==", - "dev": true, - "requires": { - "fsevents": "~2.3.2" - } - }, - "rollup-plugin-svelte": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-svelte/-/rollup-plugin-svelte-7.1.0.tgz", - "integrity": "sha512-vopCUq3G+25sKjwF5VilIbiY6KCuMNHP1PFvx2Vr3REBNMDllKHFZN2B9jwwC+MqNc3UPKkjXnceLPEjTjXGXg==", - "dev": true, - "requires": { - "require-relative": "^0.8.7", - "rollup-pluginutils": "^2.8.2" - } - }, - "rollup-pluginutils": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", - "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", - "dev": true, - "requires": { - "estree-walker": "^0.6.1" - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "sass": { - "version": "1.57.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.57.1.tgz", - "integrity": "sha512-O2+LwLS79op7GI0xZ8fqzF7X2m/m8WFfI02dHOdsK5R2ECeS5F62zrwg/relM1rjSLy7Vd/DiMNIvPrQGsA0jw==", - "dev": true, - "requires": { - "chokidar": ">=3.0.0 <4.0.0", - "immutable": "^4.0.0", - "source-map-js": ">=0.6.2 <2.0.0" - } - }, - "send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "requires": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "dependencies": { - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - } - } - }, - "serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" - } - }, - "setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - } - }, - "source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true - }, - "statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" - }, - "streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - } - } - }, - "strtok3": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-7.0.0.tgz", - "integrity": "sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ==", - "requires": { - "@tokenizer/token": "^0.3.0", - "peek-readable": "^5.0.0" - } - }, - "supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true - }, - "svelte": { - "version": "3.55.1", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.55.1.tgz", - "integrity": "sha512-S+87/P0Ve67HxKkEV23iCdAh/SX1xiSfjF1HOglno/YTbSTW7RniICMCofWGdJJbdjw3S+0PfFb1JtGfTXE0oQ==", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" - }, - "token-types": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/token-types/-/token-types-5.0.1.tgz", - "integrity": "sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg==", - "requires": { - "@tokenizer/token": "^0.3.0", - "ieee754": "^1.2.1" - } - }, - "ts-mixer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.2.tgz", - "integrity": "sha512-zvHx3VM83m2WYCE8XL99uaM7mFwYSkjR2OZti98fabHrwkjsCvgwChda5xctein3xGOyaQhtTeDq/1H/GNvF3A==" - }, - "tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" - }, - "type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - } - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" - }, - "typescript": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz", - "integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==" - }, - "undici": { - "version": "5.14.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.14.0.tgz", - "integrity": "sha512-yJlHYw6yXPPsuOH0x2Ib1Km61vu4hLiRRQoafs+WUgX1vO64vgnxiCEN9dpIrhZyHFsai3F0AEj4P9zy19enEQ==", - "requires": { - "busboy": "^1.6.0" - } - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" - }, - "ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "requires": {} - }, - "xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" - } - } -} +{ + "name": "monofile", + "version": "2.0.0-dev", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "monofile", + "version": "2.0.0-dev", + "license": "Unlicense", + "dependencies": { + "@hono/node-server": "^1.8.2", + "axios": "^0.27.2", + "bytes": "^3.1.2", + "commander": "^11.1.0", + "dotenv": "^16.0.2", + "formidable": "^3.5.1", + "hono": "^4.0.10", + "jose": "^5.2.4", + "multer": "^1.4.5-lts.1", + "node-fetch": "^3.3.2", + "nodemailer": "^6.9.3", + "range-parser": "^1.2.1", + "zod": "^3.23.5" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^2.4.6", + "@tsconfig/svelte": "^4.0.1", + "@types/body-parser": "^1.19.2", + "@types/bytes": "^3.1.1", + "@types/cookie-parser": "^1.4.3", + "@types/express": "^4.17.14", + "@types/formidable": "^3.4.5", + "@types/multer": "^1.4.7", + "@types/nodemailer": "^6.4.8", + "@types/range-parser": "^1.2.6", + "discord-api-types": "^0.37.61", + "sass": "^1.57.1", + "svelte": "^3.55.1", + "svelte-preprocess": "^5.1.3", + "tslib": "^2.6.2", + "typescript": "^5.4.5", + "vite": "^4.5.0" + }, + "engines": { + "node": ">=v21" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@hono/node-server": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.8.2.tgz", + "integrity": "sha512-h8l2TBLCPHZBUrrkosZ6L5CpBLj6zdESyF4B+zngiCDF7aZFQJ0alVbLx7jn8PCVi9EyoFf8a4hOZFi1tD95EA==", + "engines": { + "node": ">=18.14.1" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-2.4.6.tgz", + "integrity": "sha512-zO79p0+DZnXPnF0ltIigWDx/ux7Ni+HRaFOw720Qeivc1azFUrJxTl0OryXVibYNx1hCboGia1NRV3x8RNv4cA==", + "dev": true, + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^1.0.4", + "debug": "^4.3.4", + "deepmerge": "^4.3.1", + "kleur": "^4.1.5", + "magic-string": "^0.30.3", + "svelte-hmr": "^0.15.3", + "vitefu": "^0.2.4" + }, + "engines": { + "node": "^14.18.0 || >= 16" + }, + "peerDependencies": { + "svelte": "^3.54.0 || ^4.0.0", + "vite": "^4.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-1.0.4.tgz", + "integrity": "sha512-zjiuZ3yydBtwpF3bj0kQNV0YXe+iKE545QGZVTaylW3eAzFr+pJ/cwK8lZEaRp4JtaJXhD5DyWAV4AxLh6DgaQ==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": "^14.18.0 || >= 16" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^2.2.0", + "svelte": "^3.54.0 || ^4.0.0", + "vite": "^4.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@sveltejs/vite-plugin-svelte/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@sveltejs/vite-plugin-svelte/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@tsconfig/svelte": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@tsconfig/svelte/-/svelte-4.0.1.tgz", + "integrity": "sha512-B+XlGpmuAQzJqDoBATNCvEPqQg0HkO7S8pM14QDI5NsmtymzRexQ1N+nX2H6RTtFbuFgaZD4I8AAi8voGg0GLg==", + "dev": true + }, + "node_modules/@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bytes": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/bytes/-/bytes-3.1.1.tgz", + "integrity": "sha512-lOGyCnw+2JVPKU3wIV0srU0NyALwTBJlVSx5DfMQOFuuohA8y9S8orImpuIQikZ0uIQ8gehrRjxgQC1rLRi11w==", + "dev": true + }, + "node_modules/@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookie-parser": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.3.tgz", + "integrity": "sha512-CqSKwFwefj4PzZ5n/iwad/bow2hTCh0FlNAeWLtQM3JA/NX/iYagIpWG2cf1bQKQ2c9gU2log5VUCrn7LDOs0w==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.14", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.14.tgz", + "integrity": "sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.31", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz", + "integrity": "sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "node_modules/@types/formidable": { + "version": "3.4.5", + "resolved": "https://registry.npmjs.org/@types/formidable/-/formidable-3.4.5.tgz", + "integrity": "sha512-s7YPsNVfnsng5L8sKnG/Gbb2tiwwJTY1conOkJzTMRvJAlLFW1nEua+ADsJQu8N1c0oTHx9+d5nqg10WuT9gHQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/mime": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", + "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==", + "dev": true + }, + "node_modules/@types/multer": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.7.tgz", + "integrity": "sha512-/SNsDidUFCvqqcWDwxv2feww/yqhNeTRL5CVoL3jU4Goc4kKEL10T7Eye65ZqPNi4HRx8sAEX59pV1aEH7drNA==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/node": { + "version": "18.7.23", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.23.tgz", + "integrity": "sha512-DWNcCHolDq0ZKGizjx2DZjR/PqsYwAcYUJmfMWqtVU2MBMG5Mo+xFZrhGId5r/O5HOuMPyQEcM6KUBp5lBZZBg==", + "dev": true + }, + "node_modules/@types/nodemailer": { + "version": "6.4.8", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.8.tgz", + "integrity": "sha512-oVsJSCkqViCn8/pEu2hfjwVO+Gb3e+eTWjg3PcjeFKRItfKpKwHphQqbYmPQrlMk+op7pNNWPbsJIEthpFN/OQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/pug": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz", + "integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==", + "dev": true + }, + "node_modules/@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.6.tgz", + "integrity": "sha512-+0autS93xyXizIYiyL02FCY8N+KkKPhILhcUSA276HxzreZ16kl+cmwvV2qAM/PuCCwPXzOXOWhiPcw20uSFcA==", + "dev": true + }, + "node_modules/@types/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==", + "dev": true, + "dependencies": { + "@types/mime": "*", + "@types/node": "*" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "dependencies": { + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "engines": { + "node": ">=16" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-indent": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", + "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/discord-api-types": { + "version": "0.37.61", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.61.tgz", + "integrity": "sha512-o/dXNFfhBpYHpQFdT6FWzeO7pKc838QeeZ9d91CfVAtpr5XLK4B/zYxQbYgPdoMiTDvJfzcsLW5naXgmHGDNXw==", + "dev": true + }, + "node_modules/dotenv": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.2.tgz", + "integrity": "sha512-JvpYKUmzQhYoIFgK2MOnF3bciIZoItIIoryihy0rIA+H4Jy0FmgyKYAHCTN98P5ybGSJcIFbh6QKeJdtZd1qhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/es6-promise": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", + "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/formidable": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.1.tgz", + "integrity": "sha512-WJWKelbRHN41m5dumb0/k8TeAx7Id/y3a+Z7QfhxP/htI9Js5zYaEDtG8uMgG0vM0lOlqnmjE99/kfpOYi/0Og==", + "dependencies": { + "dezalgo": "^1.0.4", + "hexoid": "^1.0.0", + "once": "^1.4.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/hexoid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", + "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==", + "engines": { + "node": ">=8" + } + }, + "node_modules/hono": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.0.10.tgz", + "integrity": "sha512-sq0RFAC3Ij+bkhZu90EGAQnVI1EhohRsjo9BU+BjXLbC71GSy41JjsFqCeg8MRpO2Gdu0A4MXF5licO89tn/rw==", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/immutable": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.2.2.tgz", + "integrity": "sha512-fTMKDwtbvO5tldky9QZ2fMX7slR0mYpY5nbnFWYp0fOzDhHqhgIw9KoYgxLWsoNTS9ZHGauHj18DTyEw6BK3Og==", + "dev": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/jose": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.2.4.tgz", + "integrity": "sha512-6ScbIk2WWCeXkmzF6bRPmEuaqy1m8SbsRFMa/FLrSCkGIhj8OLVG/IH+XHVmNMx/KUo8cVWEE6oKR4dJ+S0Rkg==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/magic-string": { + "version": "0.30.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", + "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/multer": { + "version": "1.4.5-lts.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", + "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/nodemailer": { + "version": "6.9.3", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.3.tgz", + "integrity": "sha512-fy9v3NgTzBngrMFkDsKEj0r02U7jm6XfC3b52eoNV+GCrGj+s8pt5OqhiJdWKuw51zCTdiNR/IUD1z33LIIGpg==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/rollup": { + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/sander": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz", + "integrity": "sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==", + "dev": true, + "dependencies": { + "es6-promise": "^3.1.2", + "graceful-fs": "^4.1.3", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.2" + } + }, + "node_modules/sass": { + "version": "1.57.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.57.1.tgz", + "integrity": "sha512-O2+LwLS79op7GI0xZ8fqzF7X2m/m8WFfI02dHOdsK5R2ECeS5F62zrwg/relM1rjSLy7Vd/DiMNIvPrQGsA0jw==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/sorcery": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.0.tgz", + "integrity": "sha512-J69LQ22xrQB1cIFJhPfgtLuI6BpWRiWu1Y3vSsIwK/eAScqJxd/+CJlUuHQRdX2C9NGFamq+KqNywGgaThwfHw==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.14", + "buffer-crc32": "^0.2.5", + "minimist": "^1.2.0", + "sander": "^0.5.0" + }, + "bin": { + "sorcery": "bin/sorcery" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/svelte": { + "version": "3.55.1", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.55.1.tgz", + "integrity": "sha512-S+87/P0Ve67HxKkEV23iCdAh/SX1xiSfjF1HOglno/YTbSTW7RniICMCofWGdJJbdjw3S+0PfFb1JtGfTXE0oQ==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/svelte-hmr": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.3.tgz", + "integrity": "sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==", + "dev": true, + "engines": { + "node": "^12.20 || ^14.13.1 || >= 16" + }, + "peerDependencies": { + "svelte": "^3.19.0 || ^4.0.0" + } + }, + "node_modules/svelte-preprocess": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.1.3.tgz", + "integrity": "sha512-xxAkmxGHT+J/GourS5mVJeOXZzne1FR5ljeOUAMXUkfEhkLEllRreXpbl3dIYJlcJRfL1LO1uIAPpBpBfiqGPw==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@types/pug": "^2.0.6", + "detect-indent": "^6.1.0", + "magic-string": "^0.30.5", + "sorcery": "^0.11.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">= 16.0.0", + "pnpm": "^8.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.10.2", + "coffeescript": "^2.5.1", + "less": "^3.11.3 || ^4.0.0", + "postcss": "^7 || ^8", + "postcss-load-config": "^2.1.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", + "pug": "^3.0.0", + "sass": "^1.26.8", + "stylus": "^0.55.0", + "sugarss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "svelte": "^3.23.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0", + "typescript": ">=3.9.5 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "coffeescript": { + "optional": true + }, + "less": { + "optional": true + }, + "postcss": { + "optional": true + }, + "postcss-load-config": { + "optional": true + }, + "pug": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/vite": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz", + "integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==", + "dev": true, + "dependencies": { + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz", + "integrity": "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==", + "dev": true, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", + "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/zod": { + "version": "3.23.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.5.tgz", + "integrity": "sha512-fkwiq0VIQTksNNA131rDOsVJcns0pfVUjHzLrNBiF/O/Xxb5lQyEXkhZWcJ7npWsYlvs+h0jFWXXy4X46Em1JA==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json index 8402a52..142424e 100644 --- a/package.json +++ b/package.json @@ -1,42 +1,54 @@ -{ - "name": "monofile", - "version": "2.0.0-dev", - "description": "Discord-based file sharing", - "main": "index.js", - "scripts": { - "start": "node ./out/server/index.js", - "build": "tsc\nsass src/style:out/style\nrollup -c", - "test": "echo \"Error: no test specified\" && exit 1" - }, - "keywords": [], - "author": "Etcetera (https://cetera.uk)", - "license": "Unlicense", - "engines": { - "node": ">=v16.11" - }, - "dependencies": { - "@types/body-parser": "^1.19.2", - "@types/express": "^4.17.14", - "@types/multer": "^1.4.7", - "@types/nodemailer": "^6.4.8", - "axios": "^0.27.2", - "body-parser": "^1.20.0", - "bytes": "^3.1.2", - "cookie-parser": "^1.4.6", - "discord.js": "^14.7.1", - "dotenv": "^16.0.2", - "express": "^4.18.1", - "multer": "^1.4.5-lts.1", - "nodemailer": "^6.9.3", - "typescript": "^4.8.3" - }, - "devDependencies": { - "@rollup/plugin-node-resolve": "^15.0.1", - "@types/bytes": "^3.1.1", - "@types/cookie-parser": "^1.4.3", - "rollup": "^3.11.0", - "rollup-plugin-svelte": "^7.1.0", - "sass": "^1.57.1", - "svelte": "^3.55.1" - } -} +{ + "name": "monofile", + "version": "2.0.0-dev", + "description": "Discord-based file sharing", + "main": "index.js", + "type": "module", + "scripts": { + "start": "node ./out/server/index.js", + "test": "echo \"Error: no test specified\" && exit 1", + "dev": "vite", + "build": "tsc --build src/server && vite build", + "preview": "vite preview" + }, + "keywords": [], + "author": "Etcetera (https://cetera.uk)", + "license": "Unlicense", + "engines": { + "node": ">=v21" + }, + "dependencies": { + "@hono/node-server": "^1.8.2", + "axios": "^0.27.2", + "bytes": "^3.1.2", + "commander": "^11.1.0", + "dotenv": "^16.0.2", + "formidable": "^3.5.1", + "hono": "^4.0.10", + "jose": "^5.2.4", + "multer": "^1.4.5-lts.1", + "node-fetch": "^3.3.2", + "nodemailer": "^6.9.3", + "range-parser": "^1.2.1", + "zod": "^3.23.5" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^2.4.6", + "@tsconfig/svelte": "^4.0.1", + "@types/body-parser": "^1.19.2", + "@types/bytes": "^3.1.1", + "@types/cookie-parser": "^1.4.3", + "@types/express": "^4.17.14", + "@types/formidable": "^3.4.5", + "@types/multer": "^1.4.7", + "@types/nodemailer": "^6.4.8", + "@types/range-parser": "^1.2.6", + "discord-api-types": "^0.37.61", + "sass": "^1.57.1", + "svelte": "^3.55.1", + "svelte-preprocess": "^5.1.3", + "tslib": "^2.6.2", + "typescript": "^5.4.5", + "vite": "^4.5.0" + } +} diff --git a/pages/index.html b/pages/index.html deleted file mode 100644 index dc03392..0000000 --- a/pages/index.html +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - - - - - monofile - - - - - - - - - - - - - - - diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..8ffa49c --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,1459 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + '@hono/node-server': + specifier: ^1.2.0 + version: 1.2.0 + '@types/body-parser': + specifier: ^1.19.2 + version: 1.19.3 + '@types/express': + specifier: ^4.17.14 + version: 4.17.18 + '@types/multer': + specifier: ^1.4.7 + version: 1.4.8 + '@types/nodemailer': + specifier: ^6.4.8 + version: 6.4.11 + axios: + specifier: ^0.27.2 + version: 0.27.2 + body-parser: + specifier: ^1.20.0 + version: 1.20.2 + bytes: + specifier: ^3.1.2 + version: 3.1.2 + cookie-parser: + specifier: ^1.4.6 + version: 1.4.6 + discord.js: + specifier: ^14.7.1 + version: 14.13.0 + dotenv: + specifier: ^16.0.2 + version: 16.3.1 + express: + specifier: ^4.18.1 + version: 4.18.2 + hono: + specifier: ^3.8.3 + version: 3.8.3 + multer: + specifier: ^1.4.5-lts.1 + version: 1.4.5-lts.1 + nodemailer: + specifier: ^6.9.3 + version: 6.9.5 + typescript: + specifier: ^5.2.2 + version: 5.2.2 + +devDependencies: + '@sveltejs/vite-plugin-svelte': + specifier: ^2.4.6 + version: 2.4.6(svelte@3.59.2)(vite@4.5.0) + '@types/bytes': + specifier: ^3.1.1 + version: 3.1.2 + '@types/cookie-parser': + specifier: ^1.4.3 + version: 1.4.4 + '@types/range-parser': + specifier: ^1.2.6 + version: 1.2.6 + sass: + specifier: ^1.57.1 + version: 1.69.0 + svelte: + specifier: ^3.55.1 + version: 3.59.2 + vite: + specifier: ^4.5.0 + version: 4.5.0(sass@1.69.0) + +packages: + + /@discordjs/builders@1.6.5: + resolution: {integrity: sha512-SdweyCs/+mHj+PNhGLLle7RrRFX9ZAhzynHahMCLqp5Zeq7np7XC6/mgzHc79QoVlQ1zZtOkTTiJpOZu5V8Ufg==} + engines: {node: '>=16.11.0'} + dependencies: + '@discordjs/formatters': 0.3.2 + '@discordjs/util': 1.0.1 + '@sapphire/shapeshift': 3.9.2 + discord-api-types: 0.37.50 + fast-deep-equal: 3.1.3 + ts-mixer: 6.0.3 + tslib: 2.6.2 + dev: false + + /@discordjs/collection@1.5.3: + resolution: {integrity: sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==} + engines: {node: '>=16.11.0'} + dev: false + + /@discordjs/formatters@0.3.2: + resolution: {integrity: sha512-lE++JZK8LSSDRM5nLjhuvWhGuKiXqu+JZ/DsOR89DVVia3z9fdCJVcHF2W/1Zxgq0re7kCzmAJlCMMX3tetKpA==} + engines: {node: '>=16.11.0'} + dependencies: + discord-api-types: 0.37.50 + dev: false + + /@discordjs/rest@2.0.1: + resolution: {integrity: sha512-/eWAdDRvwX/rIE2tuQUmKaxmWeHmGealttIzGzlYfI4+a7y9b6ZoMp8BG/jaohs8D8iEnCNYaZiOFLVFLQb8Zg==} + engines: {node: '>=16.11.0'} + dependencies: + '@discordjs/collection': 1.5.3 + '@discordjs/util': 1.0.1 + '@sapphire/async-queue': 1.5.0 + '@sapphire/snowflake': 3.5.1 + '@vladfrangu/async_event_emitter': 2.2.2 + discord-api-types: 0.37.50 + magic-bytes.js: 1.5.0 + tslib: 2.6.2 + undici: 5.22.1 + dev: false + + /@discordjs/util@1.0.1: + resolution: {integrity: sha512-d0N2yCxB8r4bn00/hvFZwM7goDcUhtViC5un4hPj73Ba4yrChLSJD8fy7Ps5jpTLg1fE9n4K0xBLc1y9WGwSsA==} + engines: {node: '>=16.11.0'} + dev: false + + /@discordjs/ws@1.0.1: + resolution: {integrity: sha512-avvAolBqN3yrSvdBPcJ/0j2g42ABzrv3PEL76e3YTp2WYMGH7cuspkjfSyNWaqYl1J+669dlLp+YFMxSVQyS5g==} + engines: {node: '>=16.11.0'} + dependencies: + '@discordjs/collection': 1.5.3 + '@discordjs/rest': 2.0.1 + '@discordjs/util': 1.0.1 + '@sapphire/async-queue': 1.5.0 + '@types/ws': 8.5.6 + '@vladfrangu/async_event_emitter': 2.2.2 + discord-api-types: 0.37.50 + tslib: 2.6.2 + ws: 8.14.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: false + + /@esbuild/android-arm64@0.18.20: + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm@0.18.20: + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-x64@0.18.20: + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-arm64@0.18.20: + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-x64@0.18.20: + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-arm64@0.18.20: + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-x64@0.18.20: + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm64@0.18.20: + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm@0.18.20: + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ia32@0.18.20: + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-loong64@0.18.20: + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-mips64el@0.18.20: + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ppc64@0.18.20: + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-riscv64@0.18.20: + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-s390x@0.18.20: + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-x64@0.18.20: + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-x64@0.18.20: + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-x64@0.18.20: + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/sunos-x64@0.18.20: + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-arm64@0.18.20: + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-ia32@0.18.20: + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-x64@0.18.20: + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@hono/node-server@1.2.0: + resolution: {integrity: sha512-aHT8lDMLpd7ioXJ1/057+h+oE/k7rCOWmjklYDsE0jE4CoNB9XzG4f8dRHvw4s5HJFocaYDiGgYM/V0kYbQ0ww==} + engines: {node: '>=18.0.0'} + dev: false + + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + dev: true + + /@sapphire/async-queue@1.5.0: + resolution: {integrity: sha512-JkLdIsP8fPAdh9ZZjrbHWR/+mZj0wvKS5ICibcLrRI1j84UmLMshx5n9QmL8b95d4onJ2xxiyugTgSAX7AalmA==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + dev: false + + /@sapphire/shapeshift@3.9.2: + resolution: {integrity: sha512-YRbCXWy969oGIdqR/wha62eX8GNHsvyYi0Rfd4rNW6tSVVa8p0ELiMEuOH/k8rgtvRoM+EMV7Csqz77YdwiDpA==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + dependencies: + fast-deep-equal: 3.1.3 + lodash: 4.17.21 + dev: false + + /@sapphire/snowflake@3.5.1: + resolution: {integrity: sha512-BxcYGzgEsdlG0dKAyOm0ehLGm2CafIrfQTZGWgkfKYbj+pNNsorZ7EotuZukc2MT70E0UbppVbtpBrqpzVzjNA==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + dev: false + + /@sveltejs/vite-plugin-svelte-inspector@1.0.4(@sveltejs/vite-plugin-svelte@2.4.6)(svelte@3.59.2)(vite@4.5.0): + resolution: {integrity: sha512-zjiuZ3yydBtwpF3bj0kQNV0YXe+iKE545QGZVTaylW3eAzFr+pJ/cwK8lZEaRp4JtaJXhD5DyWAV4AxLh6DgaQ==} + engines: {node: ^14.18.0 || >= 16} + peerDependencies: + '@sveltejs/vite-plugin-svelte': ^2.2.0 + svelte: ^3.54.0 || ^4.0.0 + vite: ^4.0.0 + dependencies: + '@sveltejs/vite-plugin-svelte': 2.4.6(svelte@3.59.2)(vite@4.5.0) + debug: 4.3.4 + svelte: 3.59.2 + vite: 4.5.0(sass@1.69.0) + transitivePeerDependencies: + - supports-color + dev: true + + /@sveltejs/vite-plugin-svelte@2.4.6(svelte@3.59.2)(vite@4.5.0): + resolution: {integrity: sha512-zO79p0+DZnXPnF0ltIigWDx/ux7Ni+HRaFOw720Qeivc1azFUrJxTl0OryXVibYNx1hCboGia1NRV3x8RNv4cA==} + engines: {node: ^14.18.0 || >= 16} + peerDependencies: + svelte: ^3.54.0 || ^4.0.0 + vite: ^4.0.0 + dependencies: + '@sveltejs/vite-plugin-svelte-inspector': 1.0.4(@sveltejs/vite-plugin-svelte@2.4.6)(svelte@3.59.2)(vite@4.5.0) + debug: 4.3.4 + deepmerge: 4.3.1 + kleur: 4.1.5 + magic-string: 0.30.5 + svelte: 3.59.2 + svelte-hmr: 0.15.3(svelte@3.59.2) + vite: 4.5.0(sass@1.69.0) + vitefu: 0.2.5(vite@4.5.0) + transitivePeerDependencies: + - supports-color + dev: true + + /@types/body-parser@1.19.3: + resolution: {integrity: sha512-oyl4jvAfTGX9Bt6Or4H9ni1Z447/tQuxnZsytsCaExKlmJiU8sFgnIBRzJUpKwB5eWn9HuBYlUlVA74q/yN0eQ==} + dependencies: + '@types/connect': 3.4.36 + '@types/node': 20.8.3 + + /@types/bytes@3.1.2: + resolution: {integrity: sha512-92b6q7CSYBMVZDtMZh5PuKm3LjZwcU7s6H8e9sU20Z1tOrTuXN+Hz3VuP9E8axiQRaCoiEOMN1duqPCEIhamrQ==} + dev: true + + /@types/connect@3.4.36: + resolution: {integrity: sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==} + dependencies: + '@types/node': 20.8.3 + + /@types/cookie-parser@1.4.4: + resolution: {integrity: sha512-Var+aj5I6ZgIqsQ05N2V8q5OBrFfZXtIGWWDSrEYLIbMw758obagSwdGcLCjwh1Ga7M7+wj0SDIAaAC/WT7aaA==} + dependencies: + '@types/express': 4.17.18 + dev: true + + /@types/express-serve-static-core@4.17.37: + resolution: {integrity: sha512-ZohaCYTgGFcOP7u6aJOhY9uIZQgZ2vxC2yWoArY+FeDXlqeH66ZVBjgvg+RLVAS/DWNq4Ap9ZXu1+SUQiiWYMg==} + dependencies: + '@types/node': 20.8.3 + '@types/qs': 6.9.8 + '@types/range-parser': 1.2.6 + '@types/send': 0.17.2 + + /@types/express@4.17.18: + resolution: {integrity: sha512-Sxv8BSLLgsBYmcnGdGjjEjqET2U+AKAdCRODmMiq02FgjwuV75Ut85DRpvFjyw/Mk0vgUOliGRU0UUmuuZHByQ==} + dependencies: + '@types/body-parser': 1.19.3 + '@types/express-serve-static-core': 4.17.37 + '@types/qs': 6.9.8 + '@types/serve-static': 1.15.3 + + /@types/http-errors@2.0.2: + resolution: {integrity: sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg==} + + /@types/mime@1.3.3: + resolution: {integrity: sha512-Ys+/St+2VF4+xuY6+kDIXGxbNRO0mesVg0bbxEfB97Od1Vjpjx9KD1qxs64Gcb3CWPirk9Xe+PT4YiiHQ9T+eg==} + + /@types/mime@3.0.2: + resolution: {integrity: sha512-Wj+fqpTLtTbG7c0tH47dkahefpLKEbB+xAZuLq7b4/IDHPl/n6VoXcyUQ2bypFlbSwvCr0y+bD4euTTqTJsPxQ==} + + /@types/multer@1.4.8: + resolution: {integrity: sha512-VMZOW6mnmMMhA5m3fsCdXBwFwC+a+27/8gctNMuQC4f7UtWcF79KAFGoIfKZ4iqrElgWIa3j5vhMJDp0iikQ1g==} + dependencies: + '@types/express': 4.17.18 + dev: false + + /@types/node@20.8.3: + resolution: {integrity: sha512-jxiZQFpb+NlH5kjW49vXxvxTjeeqlbsnTAdBTKpzEdPs9itay7MscYXz3Fo9VYFEsfQ6LJFitHad3faerLAjCw==} + + /@types/nodemailer@6.4.11: + resolution: {integrity: sha512-Ld2c0frwpGT4VseuoeboCXQ7UJIkK3X7Lx/4YsZEiUHtHsthWAOCYtf6PAiLhMtfwV0cWJRabLBS3+LD8x6Nrw==} + dependencies: + '@types/node': 20.8.3 + dev: false + + /@types/qs@6.9.8: + resolution: {integrity: sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg==} + + /@types/range-parser@1.2.6: + resolution: {integrity: sha512-+0autS93xyXizIYiyL02FCY8N+KkKPhILhcUSA276HxzreZ16kl+cmwvV2qAM/PuCCwPXzOXOWhiPcw20uSFcA==} + + /@types/send@0.17.2: + resolution: {integrity: sha512-aAG6yRf6r0wQ29bkS+x97BIs64ZLxeE/ARwyS6wrldMm3C1MdKwCcnnEwMC1slI8wuxJOpiUH9MioC0A0i+GJw==} + dependencies: + '@types/mime': 1.3.3 + '@types/node': 20.8.3 + + /@types/serve-static@1.15.3: + resolution: {integrity: sha512-yVRvFsEMrv7s0lGhzrggJjNOSmZCdgCjw9xWrPr/kNNLp6FaDfMC1KaYl3TSJ0c58bECwNBMoQrZJ8hA8E1eFg==} + dependencies: + '@types/http-errors': 2.0.2 + '@types/mime': 3.0.2 + '@types/node': 20.8.3 + + /@types/ws@8.5.6: + resolution: {integrity: sha512-8B5EO9jLVCy+B58PLHvLDuOD8DRVMgQzq8d55SjLCOn9kqGyqOvy27exVaTio1q1nX5zLu8/6N0n2ThSxOM6tg==} + dependencies: + '@types/node': 20.8.3 + dev: false + + /@vladfrangu/async_event_emitter@2.2.2: + resolution: {integrity: sha512-HIzRG7sy88UZjBJamssEczH5q7t5+axva19UbZLO6u0ySbYPrwzWiXBcC0WuHyhKKoeCyneH+FvYzKQq/zTtkQ==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + dev: false + + /accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + dev: false + + /anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + dev: true + + /append-field@1.0.0: + resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} + dev: false + + /array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + dev: false + + /asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + dev: false + + /axios@0.27.2: + resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==} + dependencies: + follow-redirects: 1.15.3 + form-data: 4.0.0 + transitivePeerDependencies: + - debug + dev: false + + /binary-extensions@2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} + dev: true + + /body-parser@1.20.1: + resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.11.0 + raw-body: 2.5.1 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /body-parser@1.20.2: + resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.11.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /braces@3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + dev: true + + /buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + dev: false + + /busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + dependencies: + streamsearch: 1.1.0 + dev: false + + /bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + dev: false + + /call-bind@1.0.2: + resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} + dependencies: + function-bind: 1.1.1 + get-intrinsic: 1.2.1 + dev: false + + /chokidar@3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + dependencies: + delayed-stream: 1.0.0 + dev: false + + /concat-stream@1.6.2: + resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} + engines: {'0': node >= 0.8} + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 2.3.8 + typedarray: 0.0.6 + dev: false + + /content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + dev: false + + /cookie-parser@1.4.6: + resolution: {integrity: sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==} + engines: {node: '>= 0.8.0'} + dependencies: + cookie: 0.4.1 + cookie-signature: 1.0.6 + dev: false + + /cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + dev: false + + /cookie@0.4.1: + resolution: {integrity: sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==} + engines: {node: '>= 0.6'} + dev: false + + /cookie@0.5.0: + resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} + engines: {node: '>= 0.6'} + dev: false + + /core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + dev: false + + /debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.0.0 + dev: false + + /debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: true + + /deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + dev: true + + /delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + dev: false + + /depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + dev: false + + /destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dev: false + + /discord-api-types@0.37.50: + resolution: {integrity: sha512-X4CDiMnDbA3s3RaUXWXmgAIbY1uxab3fqe3qwzg5XutR3wjqi7M3IkgQbsIBzpqBN2YWr/Qdv7JrFRqSgb4TFg==} + dev: false + + /discord.js@14.13.0: + resolution: {integrity: sha512-Kufdvg7fpyTEwANGy9x7i4od4yu5c6gVddGi5CKm4Y5a6sF0VBODObI3o0Bh7TGCj0LfNT8Qp8z04wnLFzgnbA==} + engines: {node: '>=16.11.0'} + dependencies: + '@discordjs/builders': 1.6.5 + '@discordjs/collection': 1.5.3 + '@discordjs/formatters': 0.3.2 + '@discordjs/rest': 2.0.1 + '@discordjs/util': 1.0.1 + '@discordjs/ws': 1.0.1 + '@sapphire/snowflake': 3.5.1 + '@types/ws': 8.5.6 + discord-api-types: 0.37.50 + fast-deep-equal: 3.1.3 + lodash.snakecase: 4.1.1 + tslib: 2.6.2 + undici: 5.22.1 + ws: 8.14.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: false + + /dotenv@16.3.1: + resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==} + engines: {node: '>=12'} + dev: false + + /ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + dev: false + + /encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + dev: false + + /esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + dev: true + + /escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + dev: false + + /etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + dev: false + + /express@4.18.2: + resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==} + engines: {node: '>= 0.10.0'} + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.1 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.5.0 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.2.0 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.1 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.7 + proxy-addr: 2.0.7 + qs: 6.11.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.18.0 + serve-static: 1.15.0 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + dev: false + + /fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + dev: false + + /fill-range@7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: true + + /finalhandler@1.2.0: + resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} + engines: {node: '>= 0.8'} + dependencies: + debug: 2.6.9 + encodeurl: 1.0.2 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /follow-redirects@1.15.3: + resolution: {integrity: sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: false + + /form-data@4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + dev: false + + /forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + dev: false + + /fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + dev: false + + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /function-bind@1.1.1: + resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + dev: false + + /get-intrinsic@1.2.1: + resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==} + dependencies: + function-bind: 1.1.1 + has: 1.0.4 + has-proto: 1.0.1 + has-symbols: 1.0.3 + dev: false + + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: true + + /has-proto@1.0.1: + resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} + engines: {node: '>= 0.4'} + dev: false + + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + dev: false + + /has@1.0.4: + resolution: {integrity: sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==} + engines: {node: '>= 0.4.0'} + dev: false + + /hono@3.8.3: + resolution: {integrity: sha512-NLJgUCKKMvijBy+V+U1FQTsNwHk2bD1KGlWJA9+qaCNWgx5St9bhfQwxrpcTGvG2Gi2naemTWCzBavDNXOqO6Q==} + engines: {node: '>=16.0.0'} + dev: false + + /http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + dev: false + + /iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: false + + /immutable@4.3.4: + resolution: {integrity: sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==} + dev: true + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: false + + /ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + dev: false + + /is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.2.0 + dev: true + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: true + + /isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + dev: false + + /kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + dev: true + + /lodash.snakecase@4.1.1: + resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} + dev: false + + /lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + dev: false + + /magic-bytes.js@1.5.0: + resolution: {integrity: sha512-wJkXvutRbNWcc37tt5j1HyOK1nosspdh3dj6LUYYAvF6JYNqs53IfRvK9oEpcwiDA1NdoIi64yAMfdivPeVAyw==} + dev: false + + /magic-string@0.30.5: + resolution: {integrity: sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + dev: false + + /merge-descriptors@1.0.1: + resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} + dev: false + + /methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + dev: false + + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: false + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: false + + /mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + dev: false + + /minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + dev: false + + /mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + dependencies: + minimist: 1.2.8 + dev: false + + /ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + dev: false + + /ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: true + + /ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + dev: false + + /multer@1.4.5-lts.1: + resolution: {integrity: sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==} + engines: {node: '>= 6.0.0'} + dependencies: + append-field: 1.0.0 + busboy: 1.6.0 + concat-stream: 1.6.2 + mkdirp: 0.5.6 + object-assign: 4.1.1 + type-is: 1.6.18 + xtend: 4.0.2 + dev: false + + /nanoid@3.3.6: + resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + + /negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + dev: false + + /nodemailer@6.9.5: + resolution: {integrity: sha512-/dmdWo62XjumuLc5+AYQZeiRj+PRR8y8qKtFCOyuOl1k/hckZd8durUUHs/ucKx6/8kN+wFxqKJlQ/LK/qR5FA==} + engines: {node: '>=6.0.0'} + dev: false + + /normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: true + + /object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + dev: false + + /object-inspect@1.12.3: + resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} + dev: false + + /on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + dependencies: + ee-first: 1.1.1 + dev: false + + /parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + dev: false + + /path-to-regexp@0.1.7: + resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} + dev: false + + /picocolors@1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + dev: true + + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: true + + /postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.6 + picocolors: 1.0.0 + source-map-js: 1.0.2 + dev: true + + /process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + dev: false + + /proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + dev: false + + /qs@6.11.0: + resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.0.4 + dev: false + + /range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + dev: false + + /raw-body@2.5.1: + resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==} + engines: {node: '>= 0.8'} + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + dev: false + + /raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + dev: false + + /readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + dev: false + + /readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + dev: true + + /rollup@3.29.4: + resolution: {integrity: sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==} + engines: {node: '>=14.18.0', npm: '>=8.0.0'} + hasBin: true + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + dev: false + + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: false + + /safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + dev: false + + /sass@1.69.0: + resolution: {integrity: sha512-l3bbFpfTOGgQZCLU/gvm1lbsQ5mC/WnLz3djL2v4WCJBDrWm58PO+jgngcGRNnKUh6wSsdm50YaovTqskZ0xDQ==} + engines: {node: '>=14.0.0'} + hasBin: true + dependencies: + chokidar: 3.5.3 + immutable: 4.3.4 + source-map-js: 1.0.2 + dev: true + + /send@0.18.0: + resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} + engines: {node: '>= 0.8.0'} + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + dev: false + + /serve-static@1.15.0: + resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} + engines: {node: '>= 0.8.0'} + dependencies: + encodeurl: 1.0.2 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.18.0 + transitivePeerDependencies: + - supports-color + dev: false + + /setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + dev: false + + /side-channel@1.0.4: + resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + object-inspect: 1.12.3 + dev: false + + /source-map-js@1.0.2: + resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} + engines: {node: '>=0.10.0'} + dev: true + + /statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + dev: false + + /streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + dev: false + + /string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + dependencies: + safe-buffer: 5.1.2 + dev: false + + /svelte-hmr@0.15.3(svelte@3.59.2): + resolution: {integrity: sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==} + engines: {node: ^12.20 || ^14.13.1 || >= 16} + peerDependencies: + svelte: ^3.19.0 || ^4.0.0 + dependencies: + svelte: 3.59.2 + dev: true + + /svelte@3.59.2: + resolution: {integrity: sha512-vzSyuGr3eEoAtT/A6bmajosJZIUWySzY2CzB3w2pgPvnkUjGqlDnsNnA0PMO+mMAhuyMul6C2uuZzY6ELSkzyA==} + engines: {node: '>= 8'} + dev: true + + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: true + + /toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + dev: false + + /ts-mixer@6.0.3: + resolution: {integrity: sha512-k43M7uCG1AkTyxgnmI5MPwKoUvS/bRvLvUb7+Pgpdlmok8AoqmUaZxUUw8zKM5B1lqZrt41GjYgnvAi0fppqgQ==} + dev: false + + /tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + dev: false + + /type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + dev: false + + /typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + dev: false + + /typescript@5.2.2: + resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} + engines: {node: '>=14.17'} + hasBin: true + dev: false + + /undici@5.22.1: + resolution: {integrity: sha512-Ji2IJhFXZY0x/0tVBXeQwgPlLWw13GVzpsWPQ3rV50IFMMof2I55PZZxtm4P6iNq+L5znYN9nSTAq0ZyE6lSJw==} + engines: {node: '>=14.0'} + dependencies: + busboy: 1.6.0 + dev: false + + /unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + dev: false + + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + dev: false + + /utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + dev: false + + /vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + dev: false + + /vite@4.5.0(sass@1.69.0): + resolution: {integrity: sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==} + engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true + peerDependencies: + '@types/node': '>= 14' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + esbuild: 0.18.20 + postcss: 8.4.31 + rollup: 3.29.4 + sass: 1.69.0 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /vitefu@0.2.5(vite@4.5.0): + resolution: {integrity: sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + vite: + optional: true + dependencies: + vite: 4.5.0(sass@1.69.0) + dev: true + + /ws@8.14.2: + resolution: {integrity: sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + + /xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + dev: false diff --git a/rollup.config.mjs b/rollup.config.mjs deleted file mode 100644 index b8f64fd..0000000 --- a/rollup.config.mjs +++ /dev/null @@ -1,17 +0,0 @@ -import svelte from 'rollup-plugin-svelte' -import resolve from "@rollup/plugin-node-resolve" - -export default [ - { - input: "src/client/index.js", - output: { - file: 'out/client/index.js', - format: 'esm', - sourcemap:true - }, - plugins: [ - resolve({ browser: true }), - svelte({}) - ] - } -] \ No newline at end of file diff --git a/src/client/index.js b/src/client/index.js deleted file mode 100644 index f8e4e62..0000000 --- a/src/client/index.js +++ /dev/null @@ -1,5 +0,0 @@ -import App from "../svelte/App.svelte" - -new App({ - target: document.body -}) \ No newline at end of file diff --git a/pages/download.html b/src/download.html similarity index 90% rename from pages/download.html rename to src/download.html index d2cec08..dd0847a 100644 --- a/pages/download.html +++ b/src/download.html @@ -14,12 +14,12 @@ - \ No newline at end of file + diff --git a/pages/error.html b/src/error.html similarity index 83% rename from pages/error.html rename to src/error.html index 228bb87..47bfd60 100644 --- a/pages/error.html +++ b/src/error.html @@ -6,7 +6,7 @@ - \ No newline at end of file + diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..c3d2319 --- /dev/null +++ b/src/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + monofile + + + + + + + + diff --git a/src/server/index.ts b/src/server/index.ts index 68819e6..91f12bf 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1,163 +1,133 @@ -import cookieParser from "cookie-parser"; -import { IntentsBitField, Client } from "discord.js" -import express from "express" +import { serve } from "@hono/node-server" +import { serveStatic } from "@hono/node-server/serve-static" +import { Hono } from "hono" import fs from "fs" -import bytes from "bytes"; +import { readFile } from "fs/promises" +import Files from "./lib/files.js" +import APIRouter from "./routes/api.js" +import { fileURLToPath } from "url" +import { dirname } from "path" +import config from "./lib/config.js" +import { dbs } from "./lib/dbfile.js" -import ServeError from "./lib/errors" -import Files from "./lib/files" -import * as auth from "./lib/auth" -import * as Accounts from "./lib/accounts" +const app = new Hono({strict: false}) -import * as authRoutes from "./routes/authRoutes"; -import * as fileApiRoutes from "./routes/fileApiRoutes"; -import * as adminRoutes from "./routes/adminRoutes"; -import * as primaryApi from "./routes/primaryApi"; -import { getAccount } from "./lib/middleware"; +app.get( + "/static/assets/*", + serveStatic({ + rewriteRequestPath: (path) => { + return path.replace("/static/assets", "/assets") + }, + }) +) +app.get( + "/static/vite/*", + serveStatic({ + rewriteRequestPath: (path) => { + return path.replace("/static/vite", "/dist/static/vite") + }, + }) +) -require("dotenv").config() +// respond to the MOLLER method +// get it? +// haha... -let pkg = require(`${process.cwd()}/package.json`) -let app = express() -let config = require(`${process.cwd()}/config.json`) - -app.use("/static/assets",express.static("assets")) -app.use("/static/style",express.static("out/style")) -app.use("/static/js",express.static("out/client")) +app.on(["MOLLER"], "*", async (ctx) => { + ctx.header("Content-Type", "image/webp") + return ctx.body(await readFile("./assets/moller.png")) +}) //app.use(bodyParser.text({limit:(config.maxDiscordFileSize*config.maxDiscordFiles)+1048576,type:["application/json","text/plain"]})) -app.use(cookieParser()) - // check for ssl, if not redirect -if (config.trustProxy) app.enable("trust proxy") +if (config.trustProxy) { + // app.enable("trust proxy") +} if (config.forceSSL) { - app.use((req,res,next) => { - if (req.protocol == "http") res.redirect(`https://${req.get("host")}${req.originalUrl}`) - else next() + app.use(async (ctx, next) => { + if (new URL(ctx.req.url).protocol == "http") { + return ctx.redirect( + `https://${ctx.req.header("host")}${ + new URL(ctx.req.url).pathname + }` + ) + } else { + return next() + } }) } -app.get("/server",(req,res) => { - res.send(JSON.stringify({ - ...config, - version:pkg.version, - files:Object.keys(files.files).length - })) -}) - -app - .use("/auth",authRoutes.authRoutes) - .use("/admin",adminRoutes.adminRoutes) - .use("/files", fileApiRoutes.fileApiRoutes) - .use(primaryApi.primaryApi) -// funcs - -// init data - -if (!fs.existsSync(__dirname+"/../.data/")) fs.mkdirSync(__dirname+"/../.data/") - - - // discord +let files = new Files(config) -let client = new Client({intents:[ - IntentsBitField.Flags.GuildMessages, - IntentsBitField.Flags.MessageContent -],rest:{timeout:config.requestTimeout}}) +// ts screams at me if i don't +// use a function here. +// i'm inflight so +// i'm too lazy to figure this out +const apiRouter = new APIRouter(files) +apiRouter.loadAPIMethods().then(async () => + Promise.all( + Object.values(dbs) + .map(e => e.readInProgress) + .filter(e => Boolean(e)) + ) +).then(() => { + app.route("/", apiRouter.root) + console.log("API OK!") -let files = new Files(client,config) + // moved here to ensure it's matched last + app.get("/server", async (ctx) => + app.fetch( + new Request( + new URL( + "/api/v1", + ctx.req.raw.url + ).href, + ctx.req.raw + ), + ctx.env + ) + ) -authRoutes.setFilesObj(files) -adminRoutes.setFilesObj(files) -fileApiRoutes.setFilesObj(files) -primaryApi.setFilesObj(files) + app.get("/:fileId", async (ctx) => + app.fetch( + new Request( + new URL( + `/api/v1/file/${ctx.req.param("fileId")}`, + ctx.req.raw.url + ).href, + ctx.req.raw + ), + ctx.env + ) + ) -// routes (could probably make these use routers) + // listen on 3000 or PORT + // moved here to prevent a crash if someone manages to access monofile before api routes are mounted + + serve( + { + fetch: app.fetch, + port: Number(process.env.PORT || 3000), + serverOptions: { + //@ts-ignore + requestTimeout: config.requestTimeout, + }, + }, + (info) => { + console.log("Web OK!", info.port, info.address) + } + ) +}) // index, clone -app.get("/", function(req,res) { - res.sendFile(process.cwd()+"/pages/index.html") -}) - -// serve download page - -app.get("/download/:fileId", getAccount, (req,res) => { - - let acc = res.locals.acc as Accounts.Account - - if (files.getFilePointer(req.params.fileId)) { - let file = files.getFilePointer(req.params.fileId) - - if (file.visibility == "private" && acc?.id != file.owner) { - ServeError(res,403,"you do not own this file") - return - } - - fs.readFile(process.cwd()+"/pages/download.html",(err,buf) => { - let fileOwner = file.owner ? Accounts.getFromId(file.owner) : undefined; - if (err) {res.sendStatus(500);console.log(err);return} - res.send( - buf.toString() - .replace(/\$FileId/g,req.params.fileId) - .replace(/\$Version/g,pkg.version) - .replace(/\$FileSize/g,file.sizeInBytes ? bytes(file.sizeInBytes) : "[File size unknown]") - .replace(/\$FileName/g, - file.filename - .replace(/\&/g,"&") - .replace(/\/g,">") - ) - .replace(/\<\!\-\-metaTags\-\-\>/g, - ( - file.mime.startsWith("image/") - ? `` - : ( - file.mime.startsWith("video/") - ? ( - ` - - - - ` - // quick lazy fix as a fallback - // maybe i'll improve this later, but probably not. - + ((file.sizeInBytes||0) >= 26214400 ? ` - - ` : "") - ) - : "" - ) - ) - + ( - fileOwner?.embed?.largeImage && file.visibility!="anonymous" && file.mime.startsWith("image/") - ? `` - : "" - ) - + `\n` - ) - .replace(/\<\!\-\-preview\-\-\>/g, - file.mime.startsWith("image/") - ? `
` - : ( - file.mime.startsWith("video/") - ? `
` - : ( - file.mime.startsWith("audio/") - ? `
` - : "" - ) - ) - ) - .replace(/\$Uploader/g,!file.owner||file.visibility=="anonymous" ? "Anonymous" : `@${fileOwner?.username || "Deleted User"}`) - ) - }) - } else { - ServeError(res,404,"file not found") - } -}) - +app.get("/", async (ctx) => + ctx.html( + await fs.promises.readFile(process.cwd() + "/dist/index.html", "utf-8") + ) +) /* routes should be in this order: @@ -168,10 +138,4 @@ app.get("/download/:fileId", getAccount, (req,res) => { file serving */ -// listen on 3000 or MONOFILE_PORT - -app.listen(process.env.MONOFILE_PORT || 3000,function() { - console.log("Web OK!") -}) - -client.login(process.env.TOKEN) +export default app diff --git a/src/server/lib/DiscordAPI/DiscordRequests.ts b/src/server/lib/DiscordAPI/DiscordRequests.ts new file mode 100644 index 0000000..7e53f9e --- /dev/null +++ b/src/server/lib/DiscordAPI/DiscordRequests.ts @@ -0,0 +1,236 @@ +// working around typescript cause i can't think of anything better :upside_down: +import { type RequestInfo, type RequestInit, type Response, Headers } from "node-fetch" + +// I jerk off to skibidi toilet. His smile is so fucking hot, oh my god, oh. +// The voices are getting louder, help me. Oh god, i want to put it inside that toilet and make him beg. +// Whenever i see skibidi toilet cum comes out like a waterfall. +// Whenever my classmates say anything about toilets the entire school gets flooded with cum everywhere. +// Dafuqboom is truly the best artist of all time. + +let ftch_dft: (url: URL | RequestInfo, init?:RequestInit) => Promise +const fetch = async (url: URL | RequestInfo, init?:RequestInit) => { + if (ftch_dft) return ftch_dft(url, init) + else { + ftch_dft = (await import("node-fetch")).default; + return ftch_dft(url, init) + } +} + +const base = "https://discord.com/api/v10" +const buckets = new Map() +const routeConnections = new Map() + +interface RatelimitData { + bucket_name : string + limit : number + remaining : number + expires : number +} + +/** + * @description Hold a {@link REST.fetch} to be executed later + * @param rest {@link REST} to execute {@link REST.fetch|fetch()} on + * @param path Path for your request + * @param params Params for your request + * @returns An object which contains a {@link Promise}: `promise`, which resolves after `execute` is called + */ +function heldFetch( rest: REST, path: `/${string}`, params?: RequestInit ) { + let resolve: (_:Response) => any + + return { + + promise: new Promise(res => resolve = res), + + async execute() { + let response = await rest.fetch(path, params) + resolve(response) + return response + } + + } +} + +/** + * @description Extracts data on ratelimits from headers + * @param headers {@link Headers} object to extract information from + */ +function extractRatelimitData(headers: Headers): RatelimitData { + return { + bucket_name : headers.get("x-ratelimit-bucket")!, + limit : parseInt(headers.get("x-ratelimit-limit")!), + remaining : parseInt(headers.get("x-ratelimit-remaining")!), + expires : parseFloat(headers.get("x-ratelimit-reset")!), + } +} + +class DiscordAPIBucket { + + readonly name : string // bucket name (X-Ratelimit-Bucket) + readonly limit : number // bucket limit (X-Ratelimit-Limit) + remaining : number // requests remaining (X-Ratelimit-Remaining) + readonly expires : number // when this ratelimit expires (X-Ratelimit-Reset) + readonly parent : REST // parent REST + + readonly expirationHold : ReturnType // Timeout which fires after this bucket expires + dead : boolean = false // True if bucket has expired + linked_routes : `/${string}`[] = [] // Routes linked to this bucket + + constructor(rest: REST, base: Headers) { + + let rd = extractRatelimitData(base) + + this.parent = rest + this.name = rd.bucket_name || Math.random().toString() + this.limit = rd.limit + this.remaining = rd.remaining + this.expires = rd.expires + + this.expirationHold = + setTimeout( + this.destroy.bind(this), + parseFloat(base.get("x-ratelimit-reset-after")!) + ) + + } + + /** + * @description Renders this bucket invalid + */ + destroy() { + buckets.delete(this.name) + this.dead = true + this.linked_routes.forEach((v) => routeConnections.delete(v)) + Object.freeze(this) + + // execute queued requests... + // @Jack5079 i have no idea if there's a better way to do this + // fix it if there is one after you wake up + let requestsToExecute: (ReturnType["execute"])[] = [] + this.linked_routes.forEach((v) => { + let queue = this.parent.requestQueue[v] + if (queue) + requestsToExecute.push( + ...queue.splice( + 0, + Math.min( this.limit-requestsToExecute.length, queue.length ) + ) + ) + }) + requestsToExecute.forEach(a=>a()) + + } + + /** + * @description Link a route to this bucket + * @param route Route to link + */ + link(route: `/${string}`) { + if (this.linked_routes.includes(route)) return + routeConnections.set(route, this) + this.linked_routes.push(route) + } +} + +/** + * @description Returns whether or not a Response's Headers object includes Discord's ratelimit information headers + * @param headers {@link Headers} object to extract information from + */ +function checkHeaders(headers: Headers) { + return Boolean( + headers.has("x-ratelimit-bucket") + && headers.has("x-ratelimit-limit") + && headers.has("x-ratelimit-remaining") + && headers.has("x-ratelimit-reset") + && headers.has("x-ratelimit-reset-after") + ) +} + +/** + * @description Returns or creates a {@link DiscordAPIBucket} from a Response + * @param response Response or route to get a DiscordAPIBucket from + */ +function getBucket(response: string): DiscordAPIBucket | undefined +function getBucket(rest: REST, headers: Headers): DiscordAPIBucket +function getBucket(rest: REST | string, headers?: Headers) { + if (headers instanceof Headers && rest instanceof REST) { + if (!checkHeaders(headers)) throw new Error("Required ratelimiting headers not found") + + if (buckets.has(headers.get("x-ratelimit-bucket")!)) + return buckets.get(headers.get("x-ratelimit-bucket")!)! + else + return new DiscordAPIBucket(rest, headers) + } else if (typeof rest == "string") return routeConnections.get(rest) +} + +export class REST { + + private readonly token : string + requestQueue: {[key: `/${string}`]: (ReturnType["execute"])[]} = {} + + constructor(token:string) { + this.token = token; + } + + /** + * @description Queues a request + * @param path Path to request + * @param options Options for your request + */ + queue(path: `/${string}`, options?: RequestInit) { + console.warn(`Request added to queue: ${(options?.method ?? "get").toUpperCase()} ${path}`) + + let {promise, execute} = heldFetch(this, path, options) + + if (!this.requestQueue[path]) + this.requestQueue[path] = [] + + this.requestQueue[path].push(execute) + + return promise + } + + /** + * @description Make a fetch requests where further requests are automatically queued in case of ratelimit + * @param path Path to request + * @param options Options for your request + */ + async fetch(path: `/${string}`, options?: RequestInit) { + + // check if there's already a bucket, and check if it's full + let known_bucket = getBucket( path ) + + if (known_bucket) { + if (known_bucket.remaining <= 0) return this.queue(path, options) + else known_bucket.remaining-- + } + + // there's no known bucket for this route; let's carry on with the request + let response = await fetch(base+path, { + ...options, + headers: { + ...options?.headers, + Authorization: `Bot ${this.token}` + } + }) + + if ( checkHeaders(response.headers) ) { + if (response.status == 429) { + let bucket = getBucket( this, response.headers ) + bucket.link(path) // link the bucket so that hopefully no future errors occur + + return this.queue(path, options) /* it was ratelimited after all + getBucket() would have generated a DiscordAPIBucket + so this would be fine */ + } + /* commented out cause i feel like it'll cause issues + // let's update the bucket with data from the source now + let rd = extractRatelimitData( response.headers ) + bucket.remaining = rd.remaining + */ + } + + return response + + } + +} diff --git a/src/server/lib/DiscordAPI/index.ts b/src/server/lib/DiscordAPI/index.ts new file mode 100644 index 0000000..c6e2052 --- /dev/null +++ b/src/server/lib/DiscordAPI/index.ts @@ -0,0 +1,180 @@ +import { REST } from "./DiscordRequests.js" +import type { APIMessage } from "discord-api-types/v10" +import { Transform, type Readable } from "node:stream" +import type { Configuration } from "../config.js" + +const EXPIRE_AFTER = 20 * 60 * 1000 +const DISCORD_EPOCH = 1420070400000 +// Converts a snowflake ID string into a JS Date object using the provided epoch (in ms), or Discord's epoch if not provided +export function convertSnowflakeToDate( + snowflake: string | number, + epoch = DISCORD_EPOCH +) { + // Convert snowflake to BigInt to extract timestamp bits + // https://discord.com/developers/docs/reference#snowflakes + const milliseconds = BigInt(snowflake) >> 22n + return new Date(Number(milliseconds) + epoch) +} + +interface MessageCacheObject { + expire: number + object: string +} + +export class Client { + private readonly token: string + private readonly rest: REST + private readonly targetChannel: string + private readonly config: Configuration + private messageCache: Map = new Map() + + constructor(token: string, config: Configuration) { + this.token = token + this.rest = new REST(token) + this.targetChannel = config.targetChannel + this.config = config + } + + async fetchMessage(id: string, cache: boolean = true) { + if (cache && this.messageCache.has(id)) { + let cachedMessage = this.messageCache.get(id)! + if (cachedMessage.expire >= Date.now()) { + return JSON.parse(cachedMessage.object) as APIMessage + } + } + + let message = await (this.rest + .fetch(`/channels/${this.targetChannel}/messages/${id}`) + .then((res) => res.json()) as Promise) + + this.messageCache.set(id, { + object: JSON.stringify( + message + ) /* clone object so that removing ids from the array doesn't. yeah */, + expire: EXPIRE_AFTER + Date.now(), + }) + return message + } + + async deleteMessage(id: string) { + await this.rest.fetch( + `/channels/${this.targetChannel}/messages/${id}`, + { method: "DELETE" } + ) + this.messageCache.delete(id) + } + + // https://discord.com/developers/docs/resources/channel#bulk-delete-messages + // "This endpoint will not delete messages older than 2 weeks" so we need to check each id + async deleteMessages(ids: string[]) { + // Remove bulk deletable messages + + let bulkDeletable = ids.filter( + (e) => + Date.now() - convertSnowflakeToDate(e).valueOf() < + 2 * 7 * 24 * 60 * 60 * 1000 + ) + await this.rest.fetch( + `/channels/${this.targetChannel}/messages/bulk-delete`, + { + method: "POST", + body: JSON.stringify({ messages: bulkDeletable }), + } + ) + bulkDeletable.forEach(Map.prototype.delete.bind(this.messageCache)) + + // everything else, we can do manually... + // there's probably a better way to do this @Jack5079 + // fix for me if possible + await Promise.all( + ids + .map(async (e) => { + if ( + Date.now() - convertSnowflakeToDate(e).valueOf() >= + 2 * 7 * 24 * 60 * 60 * 1000 + ) { + return await this.deleteMessage(e) + } + }) + .filter(Boolean) + ) // filter based on whether or not it's undefined + } + + async send(stream: Readable) { + let bytes_sent = 0 + let file_number = 0 + let boundary = "-".repeat(20) + Math.random().toString().slice(2) + + let pushBoundary = (stream: Readable) => + stream.push( + `${file_number++ == 0 ? "" : "\r\n"}--${boundary}\r\nContent-Disposition: form-data; name="files[${file_number}]"; filename="${Math.random().toString().slice(2)}\r\nContent-Type: application/octet-stream\r\n\r\n` + ) + let boundPush = (stream: Readable, chunk: Buffer) => { + let position = 0 + console.log(`Chunk length ${chunk.byteLength}`) + + while (position < chunk.byteLength) { + if (bytes_sent % this.config.maxDiscordFileSize == 0) { + console.log("Progress is 0. Pushing boundary") + pushBoundary(stream) + } + + let capture = Math.min( + this.config.maxDiscordFileSize - + (bytes_sent % this.config.maxDiscordFileSize), + chunk.byteLength - position + ) + console.log( + `Capturing ${capture} bytes, ${chunk.subarray(position, position + capture).byteLength}` + ) + stream.push(chunk.subarray(position, position + capture)) + ;(position += capture), (bytes_sent += capture) + + console.log( + "Chunk progress:", + bytes_sent % this.config.maxDiscordFileSize, + "B" + ) + } + } + + let transformed = new Transform({ + transform(chunk, encoding, callback) { + boundPush(this, chunk) + callback() + }, + flush(callback) { + this.push(`\r\n--${boundary}--`) + callback() + }, + }) + + let controller = new AbortController() + stream.on("error", (_) => controller.abort()) + + //pushBoundary(transformed) + stream.pipe(transformed) + + let returned = await this.rest.fetch( + `/channels/${this.targetChannel}/messages`, + { + method: "POST", + body: transformed, + headers: { + "Content-Type": `multipart/form-data; boundary=${boundary}`, + }, + signal: controller.signal, + } + ) + + if (!returned.ok) { + throw new Error( + `[Message creation] ${returned.status} ${returned.statusText}` + ) + } + + let response = (await returned.json()) as APIMessage + console.log(JSON.stringify(response, null, 4)) + return response + } +} diff --git a/src/server/lib/accounts.ts b/src/server/lib/accounts.ts index c9f755f..1d10592 100644 --- a/src/server/lib/accounts.ts +++ b/src/server/lib/accounts.ts @@ -1,31 +1,17 @@ import crypto from "crypto" -import * as auth from "./auth"; +import * as auth from "./auth.js"; import { readFile, writeFile } from "fs/promises" -import { FileVisibility } from "./files"; +import { FileVisibility } from "./files.js"; +import { AccountSchemas } from "./schemas/index.js"; +import { z } from "zod" +import DbFile from "./dbfile.js"; // this is probably horrible // but i don't even care anymore -export let Accounts: Account[] = [] +export let Db = new DbFile("accounts",[]) -export interface Account { - id : string - username : string - email? : string - password : { - hash : string - salt : string - } - files : string[] - admin : boolean - defaultFileVisibility : FileVisibility - customCSS? : string - - embed? : { - color? : string - largeImage? : boolean - } -} +export type Account = z.infer /** * @description Create a new account. @@ -35,23 +21,21 @@ export interface Account { * @returns A Promise which returns the new account's ID */ -export function create(username:string,pwd:string,admin:boolean=false):Promise { - return new Promise((resolve,reject) => { - let accId = crypto.randomBytes(12).toString("hex") +export async function create(username:string,pwd:string,admin:boolean=false):Promise { + let acc: Account = { + id: crypto.randomUUID(), + username: username, + password: password.hash(pwd), + files: [], + admin: admin, + defaultFileVisibility: "public", + settings: AccountSchemas.Settings.User.parse({}) + } - Accounts.push( - { - id: accId, - username: username, - password: password.hash(pwd), - files: [], - admin: admin, - defaultFileVisibility: "public" - } - ) - - save().then(() => resolve(accId)) - }) + Db.data.push(acc) + await Db.save() + + return acc } /** @@ -60,7 +44,7 @@ export function create(username:string,pwd:string,admin:boolean=false):Promise e.username == username) + return Db.data.find(e => e.username == username) } /** @@ -69,7 +53,7 @@ export function getFromUsername(username:string) { * @returns An Account, if it exists */ export function getFromId(id:string) { - return Accounts.find(e => e.id == id) + return Db.data.find(e => e.id == id) } /** @@ -88,8 +72,8 @@ export function getFromToken(token:string) { * @param id The target account's ID */ export function deleteAccount(id:string) { - Accounts.splice(Accounts.findIndex(e => e.id == id),1) - return save() + Db.data.splice(Db.data.findIndex(e => e.id == id),1) + return Db.save() } export namespace password { @@ -117,11 +101,11 @@ export namespace password { */ export function set(id:string,password:string) { - let acc = Accounts.find(e => e.id == id) + let acc = Db.data.find(e => e.id == id) if (!acc) return acc.password = hash(password) - return save() + return Db.save() } @@ -131,7 +115,7 @@ export namespace password { * @param password Password to check */ export function check(id:string,password:string) { - let acc = Accounts.find(e => e.id == id) + let acc = Db.data.find(e => e.id == id) if (!acc) return return acc.password.hash == hash(password,acc.password.salt).hash @@ -145,16 +129,16 @@ export namespace files { * @param fileId The target file's ID * @returns Promise that resolves after accounts.json finishes writing */ - export function index(accountId:string,fileId:string) { + export function index(accountId:string,fileId:string,noWrite:boolean = false) { // maybe replace with a obj like // { x:true } // for faster lookups? not sure if it would be faster - let acc = Accounts.find(e => e.id == accountId) + let acc = Db.data.find(e => e.id == accountId) if (!acc) return if (acc.files.find(e => e == fileId)) return acc.files.push(fileId) - return save() + if (!noWrite) return Db.save() } /** @@ -165,31 +149,29 @@ export namespace files { * @returns A Promise which resolves when accounts.json finishes writing, if `noWrite` is `false` */ export function deindex(accountId:string,fileId:string, noWrite:boolean=false) { - let acc = Accounts.find(e => e.id == accountId) + let acc = Db.data.find(e => e.id == accountId) if (!acc) return let fi = acc.files.findIndex(e => e == fileId) if (fi >= 0) { acc.files.splice(fi,1) - if (!noWrite) return save() + if (!noWrite) return Db.save() } } } -/** - * @description Saves accounts.json - * @returns A promise which resolves when accounts.json finishes writing - */ -export function save() { - return writeFile(`${process.cwd()}/.data/accounts.json`,JSON.stringify(Accounts)) - .catch((err) => console.error(err)) +export type AccountResolvable = Account | string | `@${string}` + +export function resolve(obj: AccountResolvable) { + return typeof obj == "object" + ? obj + : obj.startsWith("@") + ? getFromUsername(obj.slice(1)) + : getFromId(obj) } -readFile(`${process.cwd()}/.data/accounts.json`) - .then((buf) => { - Accounts = JSON.parse(buf.toString()) - }).catch(err => console.error(err)) - .finally(() => { - if (!Accounts.find(e => e.admin)) { +Db.read() + .then(() => { + if (!Db.data.find(e => e.admin)) { create("admin","admin",true) } }) \ No newline at end of file diff --git a/src/server/lib/apply.ts b/src/server/lib/apply.ts new file mode 100644 index 0000000..3d942ee --- /dev/null +++ b/src/server/lib/apply.ts @@ -0,0 +1,59 @@ +import type Files from "./files.js" +import type { FilePointer } from "./files.js" +import * as Accounts from "./accounts.js" +import { FileSchemas } from "./schemas/index.js" + +export type Update = Pick + & { + owner: string | null, + id: string + } + +export function applyTagMask(tags: string[], mask: Record) { + return Object.entries(Object.assign( + Object.fromEntries(tags.map(e => [e, true])), + mask + )).filter(e => e[1]).map(e => e[0]) +} + +export const operations : Exclude<({ + [K in keyof Update]: [K, + ((files: Files, passed: Update[K], id: string, file: FilePointer) => void) + | true + ] +})[keyof Update], undefined>[] = [ + ["filename", true], + ["visibility", true], + ["tag", true], + ["owner", (files: Files, owner: string|null, id: string, file: FilePointer) => { + files.chown(id, owner || undefined, true) + return + }], + ["id", (files: Files, newId: string, oldId: string, file: FilePointer) => { + files.mv(oldId, newId, true) + return + }] +] + +export default function apply( + files: Files, + uploadId: string, + source: Partial, + noWrite: boolean = false +) { + let file = files.db.data[uploadId] + let issues = operations.map(([k, v]) => { + if (source[k] === undefined) return + if (v == true) + //@ts-ignore SHUTUPSHUTUPSHUTUP + file[k] = source[k] + else + //@ts-ignore oh my god you shut up too + v(files, source[k], uploadId, file) + }).filter(e => Boolean(e)) + + if (!noWrite) { + Accounts.Db.save() + files.db.save() + } +} \ No newline at end of file diff --git a/src/server/lib/auth.ts b/src/server/lib/auth.ts index 75a4fd8..f0e133a 100644 --- a/src/server/lib/auth.ts +++ b/src/server/lib/auth.ts @@ -1,106 +1,123 @@ import crypto from "crypto" -import express from "express" +import { getCookie } from "hono/cookie" +import type { Context } from "hono" import { readFile, writeFile } from "fs/promises" -export let AuthTokens: AuthToken[] = [] -export let AuthTokenTO:{[key:string]:NodeJS.Timeout} = {} +import { z } from "zod" +import { AuthSchemas } from "./schemas/index.js" +import DbFile from "./dbfile.js" +import * as jose from "jose" +import { AccountResolvable, resolve as resolveAccount } from "./accounts.js" +import config from "./config.js" +export let AuthTokenTO: { [key: string]: NodeJS.Timeout } = {} -export const ValidTokenPermissions = [ - "user", // permissions to /auth/me, with email docked - "email", // adds email back to /auth/me - "private", // allows app to read private files - "upload", // allows an app to upload under an account - "manage", // allows an app to manage an account's files - "customize", // allows an app to change customization settings - "admin" // only available for accounts with admin - // gives an app access to all admin tools -] as const +export type Scope = z.infer +export type TokenType = z.infer +export type AuthToken = z.infer +export type TokenResolvable = string | AuthToken -export type TokenType = "User" | "App" -export type TokenPermission = typeof ValidTokenPermissions[number] +export const Db = new DbFile("tokens", []) -export interface AuthToken { - account: string, - token: string, - expire: number, - - type?: TokenType, // if !type, assume User - tokenPermissions?: TokenPermission[] // default to user if type is App, - // give full permissions if type is User +export function resolve(token: TokenResolvable, forCleanup?: boolean) { + let resolved = typeof token == "object" ? token : Db.data.find(e => e.id == token) + if (resolved && (forCleanup || resolved.expire == null || Date.now() < resolved.expire)) + return resolved } export function create( - id:string, - expire:number=(24*60*60*1000), - type:TokenType="User", - tokenPermissions?:TokenPermission[] + account: AccountResolvable, + expire: number | null = 24 * 60 * 60 * 1000, + type: TokenType = "User", + scopes?: Scope[] ) { - let token = { - account:id, - token:crypto.randomBytes(36).toString('hex'), - expire: expire ? Date.now()+expire : 0, - + let token = AuthSchemas.AuthToken.parse({ + account: resolveAccount(account)?.id, + id: crypto.randomUUID(), + expire: typeof expire == "number" ? Date.now() + expire : null, type, - tokenPermissions: type == "App" ? tokenPermissions || ["user"] : undefined - } - - AuthTokens.push(token) + scopes: + type != "User" ? scopes || ["user"] : undefined + }) + + Db.data.push(token) tokenTimer(token) - save() + Db.save() - return token.token + return token } -export function tokenFor(req: express.Request) { - return req.cookies.auth || ( - req.header("authorization")?.startsWith("Bearer ") - ? req.header("authorization")?.split(" ")[1] - : undefined - ) + +export async function getJwtId(jwt: string) { + let result = await jose.jwtVerify(jwt, config.jwtSecret).catch(e => null) + return result ? result.payload.jti : undefined } -function getToken(token:string) { - return AuthTokens.find(e => e.token == token && (e.expire == 0 || Date.now() < e.expire)) +export function makeJwt(_token: TokenResolvable) { + let token = resolve(_token)! + let jwt = new jose.SignJWT({ + exp: token.expire ? token.expire/1000 : undefined, + sub: token.account, + jti: token.id, + ...(token.type != "User" ? { scope: token.scopes } : {}) + }).setProtectedHeader({ alg: "HS256" }) + + return jwt.sign(config.jwtSecret) } -export function validate(token:string) { - return getToken(token)?.account +export async function tokenFor(ctx: Context) { + let token = + getCookie(ctx, "auth") + || (ctx.req.header("authorization")?.startsWith("Bearer ") + ? ctx.req.header("authorization")?.split(" ")[1] + : undefined) + if (!token) return + + let jti = await getJwtId(token) + return jti } -export function getType(token:string): TokenType | undefined { - return getToken(token)?.type +export function validate(token: TokenResolvable) { + return resolve(token)?.account } -export function getPermissions(token:string): TokenPermission[] | undefined { - return getToken(token)?.tokenPermissions +export function getType(token: TokenResolvable) { + return resolve(token)?.type } -export function tokenTimer(token:AuthToken) { - if (!token.expire) return // justincase +export function getScopes(token: TokenResolvable): Scope[] | undefined { + let tok = resolve(token) + if (tok && "scopes" in tok) + return tok.scopes +} + +export function tokenTimer(token: AuthToken) { + if (!token.expire) return + if (Date.now() >= token.expire) { - invalidate(token.token) + invalidate(token) return } - AuthTokenTO[token.token] = setTimeout(() => invalidate(token.token),token.expire-Date.now()) + AuthTokenTO[token.id] = setTimeout( + () => invalidate(token), + token.expire - Date.now() + ) } -export function invalidate(token:string) { - if (AuthTokenTO[token]) { - clearTimeout(AuthTokenTO[token]) +export function invalidate(_token: TokenResolvable) { + let token = resolve(_token, true)! + if (AuthTokenTO[token.id]) { + clearTimeout(AuthTokenTO[token.id]) } - AuthTokens.splice(AuthTokens.findIndex(e => e.token == token),1) - save() + Db.data.splice( + Db.data.indexOf(token), + 1 + ) + Db.save() } -export function save() { - writeFile(`${process.cwd()}/.data/tokens.json`,JSON.stringify(AuthTokens)) - .catch((err) => console.error(err)) -} - -readFile(`${process.cwd()}/.data/tokens.json`) - .then((buf) => { - AuthTokens = JSON.parse(buf.toString()) - AuthTokens.forEach(e => tokenTimer(e)) - }).catch(err => console.error(err)) \ No newline at end of file +Db.read() + .then(() => { + Db.data.forEach((e) => tokenTimer(e)) + }) diff --git a/src/server/lib/codes.ts b/src/server/lib/codes.ts new file mode 100644 index 0000000..573d21e --- /dev/null +++ b/src/server/lib/codes.ts @@ -0,0 +1,88 @@ +import { generateFileId } from "./files.js"; +import crypto from "node:crypto" + +export type Intent = "verifyEmail" | "recoverAccount" | "identityProof" + +export const Intents = { + verifyEmail: { + limit: 2 + }, + recoverAccount: {}, + identityProof: { + codeGenerator: crypto.randomUUID + } +} as Record string, limit?: number}> + +export function isIntent(intent: string): intent is Intent { + return intent in Intents +} + +export let codes = Object.fromEntries( + Object.keys(Intents).map((e) => [ + e, + { + byId: new Map(), + byUser: new Map(), + }, + ]) +) as Record< + Intent, + { byId: Map; byUser: Map } +> + +// this is stupid whyd i write this + +export class Code { + readonly id: string + readonly for: string + readonly intent: Intent + readonly expiryClear: NodeJS.Timeout + readonly data: any + + constructor( + intent: Intent, + forUser: string, + data?: any, + time: number = 15 * 60 * 1000 + ) { + const { codeGenerator = () => generateFileId(12) } = Intents[intent] + + this.for = forUser + this.intent = intent + this.expiryClear = setTimeout(this.terminate.bind(this), time) + this.data = data + this.id = codeGenerator() + + let byUser = codes[intent].byUser.get(forUser) + if (!byUser) { + byUser = [] + codes[intent].byUser.set(forUser, byUser) + } + + codes[intent].byId.set(this.id, this) + + byUser.push(this) + } + + terminate() { + codes[this.intent].byId.delete(this.id) + let bu = codes[this.intent].byUser.get(this.for)! + bu.splice(bu.indexOf(this), 1) + clearTimeout(this.expiryClear) + } + + check(forUser: string) { + return forUser === this.for + } +} + +export function code(...params: ConstructorParameters): { success: true, code: Code } | { success: false, error: string } { + const [intent, forUser] = params + const {limit = 100} = Intents[intent] + const {length: codeCount} = codes[intent].byUser.get(forUser) || []; + + if (codeCount >= limit) + return { success: false, error: `Too many active codes for intent ${intent} (${limit})` } + else + return { success: true, code: new Code(...params) } +} \ No newline at end of file diff --git a/src/server/lib/config.ts b/src/server/lib/config.ts new file mode 100644 index 0000000..42bbf9f --- /dev/null +++ b/src/server/lib/config.ts @@ -0,0 +1,79 @@ +import "dotenv/config" + +export interface Configuration { + port: number + requestTimeout: number + trustProxy: boolean + forceSSL: boolean + discordToken: string + maxDiscordFiles: number + maxDiscordFileSize: number + maxUploadIdLength: number + targetChannel: string + accounts: { + registrationEnabled: boolean + requiredForUpload: boolean + } + mail: { + enabled: boolean + transport: { + host: string + port: number + secure: boolean + } + send: { + from: string + } + user: string + pass: string + }, + + jwtSecret: Buffer +} + +export interface ClientConfiguration { + version: string + files: number + totalSize: number + mailEnabled: boolean + maxDiscordFiles: number + maxDiscordFileSize: number + accounts: { + registrationEnabled: boolean + requiredForUpload: boolean + } +} + +export default { + port: Number(process.env.PORT), + requestTimeout: Number(process.env.REQUEST_TIMEOUT), + trustProxy: process.env.TRUST_PROXY === "true", + forceSSL: process.env.FORCE_SSL === "true", + discordToken: process.env.DISCORD_TOKEN, + maxDiscordFiles: Number(process.env.MAX__DISCORD_FILES), + maxDiscordFileSize: Number(process.env.MAX__DISCORD_FILE_SIZE), + maxUploadIdLength: Number(process.env.MAX__UPLOAD_ID_LENGTH), + targetChannel: process.env.TARGET__CHANNEL, + accounts: { + registrationEnabled: + process.env.ACCOUNTS__REGISTRATION_ENABLED === "true", + requiredForUpload: process.env.ACCOUNTS__REQUIRED_FOR_UPLOAD === "true", + }, + + mail: { + enabled: ["HOST","PORT","SEND_FROM","USER","PASS"].every(e => Boolean(process.env[`MAIL__${e}`])), + + transport: { + host: process.env.MAIL__HOST, + port: Number(process.env.MAIL__PORT), + secure: process.env.MAIL__SECURE === "true", + }, + send: { + from: process.env.MAIL__SEND_FROM, + }, + user: process.env.MAIL__USER, + pass: process.env.MAIL__PASS, + }, + + jwtSecret: Buffer.from(process.env.JWT_SECRET!) +} as Configuration diff --git a/src/server/lib/dbfile.ts b/src/server/lib/dbfile.ts new file mode 100644 index 0000000..a336c2d --- /dev/null +++ b/src/server/lib/dbfile.ts @@ -0,0 +1,177 @@ +import { readFile, writeFile, readdir, mkdir } from "fs/promises" +import { existsSync } from "fs" +import path from "node:path" + +const DATADIR = `./.data` +const TICK = 500 +export let dbs: Record> = {} + +export type Write = ReturnType + +// this is fucking stupid why did i write this + +class Activity { + + _write: () => Promise + destroy: () => void + + goal: number = Date.now() + lastWrite: number = Date.now() + clock? : { type: "precise", id: NodeJS.Timeout } | { type: "tick", id: NodeJS.Timeout, lastGoal: number } + + constructor(writeFunc: () => Promise, destroyFunc: () => void) { + this._write = writeFunc + this.destroy = destroyFunc + } + + write() { + this.lastWrite = Date.now(); + return this._write() + } + + finish() { + this.stopClock() + this.write() + this.destroy() + } + + tick() { + if (!this.clock || !("lastGoal" in this.clock)) return + if (Date.now() > this.goal) return this.finish(); + + if (this.goal == this.clock.lastGoal) + this.startPreciseClock() + else + this.clock.lastGoal = this.goal + + if (Date.now()-this.lastWrite > 15000) + this.write() + } + + stopClock() { + if (this.clock) clearTimeout(this.clock.id) + } + + startTickClock() { + this.stopClock() + this.clock = { + type: "tick", + id: setInterval(this.tick.bind(this), TICK), + lastGoal: this.goal + } + } + + startPreciseClock() { + this.stopClock() + this.clock = { + type: "precise", + id: setTimeout(this.finish.bind(this), this.goal-Date.now()) + } + } + + set() { + this.goal = Date.now()+5000 + if (!this.clock || this.clock.type != "tick") + this.startTickClock() + } + +} + +export default class DbFile { + + name: string + data: Structure + activity?: Activity + + private writeInProgress?: Promise + private rewriteNeeded: boolean = false + private readonly files: string[] + + readInProgress?: Promise + + constructor(name: string, defaultData: Structure) { + this.name = name + this.data = defaultData + this.files = [`${name}.json`, `${name}-b.json`].map(e => path.join(DATADIR, e)) + + dbs[this.name] = this + } + + private async findAvailable() { + // would it be worth it to remove existsSync here? + // mkdir seems to already do it itself when recursive is true + if (!existsSync(DATADIR)) + await mkdir(DATADIR, { recursive: true }) + + return (await readdir(DATADIR)) + .filter(e => e.match(new RegExp(`^${this.name}(?:-b)?.json$`))) + .map(e => path.join(DATADIR, e)) + } + + /** + * @description Write files to disk; doesn't care about preventing corruption aside from the 2 copies + */ + private async write() { + + let data = JSON.stringify(this.data) + for (let x of this.files) + await writeFile(x, data) + + } + + /** + * @description Write files to disk; checks if a write is in progress first + */ + private async queueWrite(): Promise { + if (this.writeInProgress) { // if write in progress + this.rewriteNeeded = true // signify that a rewrite is needed + return this.writeInProgress + } + + this.writeInProgress = this.write() + await this.writeInProgress; // wait for it to complete + delete this.writeInProgress; // then remove it + + if (this.rewriteNeeded) return this.queueWrite() // queues up another write if needed + } + + /** + * @description Starts saving data to disk + */ + async save() { + if (!this.activity) + this.activity = + new Activity( + this.queueWrite.bind(this), + () => delete this.activity + ) + + this.activity.set() + } + + private async tryRead(path: string) { + return JSON.parse((await readFile(path)).toString()) + } + + private async _read() { + let availFiles = await this.findAvailable() + + if (availFiles.length == 0) return + + for (let x of availFiles) { + let data = await this.tryRead(x).catch(_ => null) + if (data !== null) { + this.data = data + return + } + } + + throw new Error(`Failed to read any of the available files for DbFile ${this.name}`) + } + + read() { + this.readInProgress = this._read() + return this.readInProgress + } + +} \ No newline at end of file diff --git a/src/server/lib/errors.ts b/src/server/lib/errors.ts index 84c1cc5..4f40d8c 100644 --- a/src/server/lib/errors.ts +++ b/src/server/lib/errors.ts @@ -1,48 +1,36 @@ -import { Response } from "express"; import { readFile } from "fs/promises" +import type { Context } from "hono" +import type { StatusCode } from "hono/utils/http-status" -let errorPage:string +let errorPage: string /** * @description Serves an error as a response to a request with an error page attached - * @param res Express response object + * @param ctx Express response object * @param code Error code * @param reason Error reason */ export default async function ServeError( - res:Response, - code:number, - reason:string + ctx: Context, + code: number, + reason: string ) { // fetch error page if not cached - if (!errorPage) { - errorPage = - ( - await readFile(`${process.cwd()}/pages/error.html`) - .catch((err) => console.error(err)) - || "
$code $text
" - ) - .toString() - } + errorPage ??= ( + (await readFile(`${process.cwd()}/dist/error.html`).catch((err) => + console.error(err) + )) ?? "
$code $text
" + ).toString() + // serve error - res.statusMessage = reason - res.status(code) - res.header("x-backup-status-message", reason) // glitch default nginx configuration - res.send( + return ctx.req.header("accept")?.includes("text/html") ? ctx.html( errorPage - .replace(/\$code/g,code.toString()) - .replace(/\$text/g,reason) - ) + .replaceAll("$code", code.toString()) + .replaceAll("$text", reason), + code as StatusCode/*, + { + "x-backup-status-message": reason, // glitch default nginx configuration + }*/ + ) : ctx.text(reason, code as StatusCode) } -/** - * @description Redirects a user to another page. - * @param res Express response object - * @param url Target URL - * @deprecated Use `res.redirect` instead. - */ -export function Redirect(res:Response,url:string) { - res.status(302) - res.header("Location",url) - res.send() -} \ No newline at end of file diff --git a/src/server/lib/files.ts b/src/server/lib/files.ts index 259761e..0182b32 100644 --- a/src/server/lib/files.ts +++ b/src/server/lib/files.ts @@ -1,428 +1,659 @@ -import axios from "axios"; -import Discord, { Client, TextBasedChannel } from "discord.js"; -import { readFile, writeFile } from "fs"; -import { Readable } from "node:stream"; -import crypto from "node:crypto"; -import { files } from "./accounts"; +import { readFile, writeFile } from "node:fs/promises" +import { Readable, Writable } from "node:stream" +import crypto from "node:crypto" +import { files } from "./accounts.js" +import { Client as API, convertSnowflakeToDate } from "./DiscordAPI/index.js" +import type { APIAttachment } from "discord-api-types/v10" +import config, { Configuration } from "./config.js" +import "dotenv/config" +import apply from "./apply.js" -import * as Accounts from "./accounts"; +import * as Accounts from "./accounts.js" +import { z } from "zod" +import * as schemas from "./schemas/files.js" +import { issuesToMessage } from "./middleware.js" +import DbFile from "./dbfile.js" -export let id_check_regex = /[A-Za-z0-9_\-\.\!\=\:\&\$\,\+\;\@\~\*\(\)\']+/ -export let alphanum = Array.from("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890") +export let alphanum = Array.from( + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" +) // bad solution but whatever -export type FileVisibility = "public" | "anonymous" | "private" +export type FileVisibility = z.infer /** * @description Generates an alphanumeric string, used for files * @param length Length of the ID * @returns a random alphanumeric string */ -export function generateFileId(length:number=5) { +export function generateFileId(length: number = 5): z.infer { let fid = "" for (let i = 0; i < length; i++) { - fid += alphanum[crypto.randomInt(0,alphanum.length)] + fid += alphanum[crypto.randomInt(0, alphanum.length)] } return fid } -export interface FileUploadSettings { - name?: string, - mime: string, - uploadId?: string, - owner?:string -} - -export interface Configuration { - maxDiscordFiles: number, - maxDiscordFileSize: number, - targetGuild: string, - targetChannel: string, - requestTimeout: number, - maxUploadIdLength: number, - - accounts: { - registrationEnabled: boolean, - requiredForUpload: boolean - }, - - trustProxy: boolean, - forceSSL: boolean -} - -export interface FilePointer { - filename:string, - mime:string, - messageids:string[], - owner?:string, - sizeInBytes?:number, - tag?:string, - visibility?:FileVisibility, - reserved?: boolean, - chunkSize?: number -} +export type FilePointer = z.infer export interface StatusCodeError { - status: number, + status: number message: string } -/* */ +export class WebError extends Error { + readonly statusCode: number = 500 -export default class Files { + constructor(status: number, message: string) { + super(message) + this.statusCode = status + } +} - config: Configuration - client: Client - files: {[key:string]:FilePointer} = {} - uploadChannel?: TextBasedChannel +export class ReadStream extends Readable { + files: Files + pointer: FilePointer - constructor(client: Client, config: Configuration) { + attachmentBuffer: APIAttachment[] = [] + msgIdx: number = 0 + position: number = 0 - this.config = config; - this.client = client; + ranges: { + useRanges: boolean + byteStart: number + byteEnd: number + scan_msg_begin: number + scan_msg_end: number + scan_files_begin: number + scan_files_end: number + } - client.on("ready",() => { - console.log("Discord OK!") - - client.guilds.fetch(config.targetGuild).then((g) => { - g.channels.fetch(config.targetChannel).then((a) => { - if (a?.isTextBased()) { - this.uploadChannel = a + id: number = Math.random() + aborter?: AbortController + + constructor( + files: Files, + pointer: FilePointer, + range?: { start: number; end: number } + ) { + super() + console.log(this.id, range) + this.files = files + this.pointer = pointer + + let useRanges = Boolean( + range && pointer.chunkSize && pointer.sizeInBytes + ) + + this.ranges = { + useRanges, + scan_msg_begin: 0, + scan_msg_end: pointer.messageids.length - 1, + scan_files_begin: useRanges + ? Math.floor(range!.start / pointer.chunkSize!) + : 0, + scan_files_end: useRanges + ? Math.ceil(range!.end / pointer.chunkSize!) - 1 + : -1, + byteStart: range?.start || 0, + byteEnd: range?.end || 0, + } + + if (useRanges) + (this.ranges.scan_msg_begin = Math.floor( + this.ranges.scan_files_begin / 10 + )), + (this.ranges.scan_msg_end = Math.ceil( + this.ranges.scan_files_end / 10 + )), + (this.msgIdx = this.ranges.scan_msg_begin) + + console.log(this.ranges) + } + + async _read() { + /* + console.log("Calling for more data") + if (this.busy) return + this.busy = true + let readyForMore = true + + while (readyForMore) { + let result = await this.pushData() + if (result === undefined) return // stream has been destroyed. nothing left to do... + readyForMore = result + } + this.busy = false*/ + this.pushData() + } + + async _destroy( + error: Error | null, + callback: (error?: Error | null | undefined) => void + ): Promise { + if (this.aborter) this.aborter.abort() + callback() + } + + async getNextAttachment() { + // return first in our attachment buffer + let ret = this.attachmentBuffer.splice(0, 1)[0] + if (ret) return ret + + console.log( + this.id, + this.msgIdx, + this.ranges.scan_msg_end, + this.pointer.messageids[this.msgIdx] + ) + + // oh, there's none left. let's fetch a new message, then. + if ( + !this.pointer.messageids[this.msgIdx] || + this.msgIdx > this.ranges.scan_msg_end + ) + return null + + let msg = await this.files.api + .fetchMessage(this.pointer.messageids[this.msgIdx]) + .catch((e) => { + console.error(e) + return null + }) + + if (msg?.attachments) { + let attach = msg.attachments + console.log(attach) + + this.attachmentBuffer = this.ranges.useRanges + ? attach.slice( + this.msgIdx == this.ranges.scan_msg_begin + ? this.ranges.scan_files_begin - + this.ranges.scan_msg_begin * 10 + : 0, + this.msgIdx == this.ranges.scan_msg_end + ? this.ranges.scan_files_end - + this.ranges.scan_msg_end * 10 + + 1 + : attach.length + ) + : attach + + console.log(this.attachmentBuffer) + } + + this.msgIdx++ + return this.attachmentBuffer.splice(0, 1)[0] + } + + async getPusherForWebStream(webStream: ReadableStream) { + const reader = await webStream.getReader() + let pushing = false // acts as a debounce just in case + // (words of a girl paranoid from writing readfilestream) + + let pushToStream = this.push.bind(this) + let stream = this + + return function () { + if (pushing) return + pushing = true + + return reader + .read() + .catch((e) => { + // Probably means an AbortError; whatever it is we'll need to abort + if (webStream.locked) reader.releaseLock() + webStream.cancel().catch((e) => undefined) + if (!stream.destroyed) stream.destroy() + return e + }) + .then((result) => { + if (result instanceof Error || !result) return result + + let pushed + if (!result.done) { + pushing = false + pushed = pushToStream(result.value) + } + return { + readyForMore: pushed || false, + streamDone: result.done, } }) - }) - }) - - readFile(process.cwd()+"/.data/files.json",(err,buf) => { - if (err) {console.log(err);return} - this.files = JSON.parse(buf.toString() || "{}") - }) - + } } - + + async getNextChunk() { + let scanning_chunk = await this.getNextAttachment() + console.log( + this.id, + "Next chunk requested; got attachment", + scanning_chunk + ) + if (!scanning_chunk) return null + + let { byteStart, byteEnd, scan_files_begin, scan_files_end } = + this.ranges + + let headers: HeadersInit = this.ranges.useRanges + ? { + Range: `bytes=${ + this.position == 0 + ? byteStart - + scan_files_begin * this.pointer.chunkSize! + : "0" + }-${ + this.attachmentBuffer.length == 0 && + this.msgIdx == scan_files_end + ? byteEnd - scan_files_end * this.pointer.chunkSize! + : "" + }`, + } + : {} + + this.aborter = new AbortController() + + let response = await fetch(scanning_chunk.url, { + headers, + signal: this.aborter.signal, + }).catch((e: Error) => { + console.error(e) + return { body: e } + }) + + this.position++ + + return response.body + } + + currentPusher?: () => + | Promise<{ readyForMore: boolean; streamDone: boolean } | void> + | undefined + busy: boolean = false + + async pushData(): Promise { + // uh oh, we don't have a currentPusher + // let's make one then + if (!this.currentPusher) { + let next = await this.getNextChunk() + if (next && !(next instanceof Error)) { + // okay, so we have a new chunk + // let's generate a new currentPusher + this.currentPusher = await this.getPusherForWebStream(next) + } else { + // oops, look like there's an error + // or the stream has ended. + // let's destroy the stream + console.log(this.id, "Ending", next) + if (next) this.destroy(next) + else this.push(null) + return + } + } + + let result = await this.currentPusher() + + if (result?.streamDone) { + this.aborter = undefined + this.currentPusher = undefined + return this.pushData() + } else return result?.readyForMore + } +} + +export class UploadStream extends Writable { + uploadId?: string + name?: string + mime?: string + owner?: string + + files: Files + + error?: Error + + hash: crypto.Hash = crypto.createHash("md5") + + constructor(files: Files, owner?: string) { + super() + this.owner = owner + this.files = files + } + + // implementing some stuff + + async _write(data: Buffer, encoding: string, callback: () => void) { + console.log("Write to stream attempted") + if ( + this.filled + data.byteLength > + this.files.config.maxDiscordFileSize * + this.files.config.maxDiscordFiles + ) + return this.destroy(new WebError(413, "maximum file size exceeded")) + + this.hash.update(data) + + // cut up the buffer into message sized chunks + + let position = 0 + let readyForMore = false + + while (position < data.byteLength) { + let capture = Math.min( + this.files.config.maxDiscordFileSize * 10 - + (this.filled % (this.files.config.maxDiscordFileSize * 10)), + data.byteLength - position + ) + console.log( + `Capturing ${capture} bytes for megachunk, ${data.subarray(position, position + capture).byteLength}` + ) + if (!this.current) await this.getNextStream() + if (!this.current) { + this.destroy(new Error("getNextStream called during debounce")) + return + } + + readyForMore = this.current.push( + data.subarray(position, position + capture) + ) + console.log(`pushed ${data.byteLength} byte chunk`) + ;(position += capture), (this.filled += capture) + + // message is full, so tell the next run to get a new message + if ( + this.filled % (this.files.config.maxDiscordFileSize * 10) == + 0 + ) { + this.current!.push(null) + this.current = undefined + } + } + + if (readyForMore || !this.current) callback() + else this.once("exec-callback", callback) + } + + async _final(callback: (error?: Error | null | undefined) => void) { + if (this.current) { + this.current.push(null) + // i probably dnt need this but whateverrr :3 + await new Promise((res, rej) => this.once("debounceReleased", res)) + } + callback() + } + + aborted: boolean = false + + async _destroy( + error: Error | null, + callback: (err?: Error | null) => void + ) { + this.error = error || undefined + await this.abort() + callback(error) + } + /** - * @description Uploads a new file - * @param settings Settings for your new upload - * @param fBuffer Buffer containing file content - * @returns Promise which resolves to the ID of the new file + * @description Cancel & unlock the file. When destroy() is called with a non-WebError, this is automatically called */ - uploadFile(settings:FileUploadSettings,fBuffer:Buffer):Promise { - return new Promise(async (resolve,reject) => { - if (!this.uploadChannel) { - reject({status:503,message:"server is not ready - please try again later"}) - return + async abort() { + if (this.aborted) return + this.aborted = true + console.log("Aborting") + if (!this.destroyed) this.destroy() + if (this.current) this.current.destroy(this.error) + await this.files.api.deleteMessages(this.messages) + if (this.uploadId) { + delete this.files.locks[this.uploadId] + } + } + + /** + * @description Commit the file to the database + * @returns The file's ID + */ + async commit() { + if (this.errored) throw this.error + if (!this.writableFinished) { + let err = Error( + "attempted to commit file when the stream was still unfinished" + ) + if (!this.destroyed) { + this.destroy(err) } + throw err + } - if (!settings.name || !settings.mime) { - reject({status:400,message:"missing name/mime"}); - return - } + // Perform checks + if (!this.mime) { + this.abort() + throw new WebError(400, "no mime provided") + } + if (!this.name) { + this.abort() + throw new WebError(400, "no filename provided") + } - if (!settings.owner && this.config.accounts.requiredForUpload) { - reject({status:401,message:"an account is required for upload"}); - return - } - - let uploadId = (settings.uploadId || generateFileId()).toString(); - - if ((uploadId.match(id_check_regex) || [])[0] != uploadId || uploadId.length > this.config.maxUploadIdLength) { - reject({status:400,message:"invalid id"});return - } - - if (this.files[uploadId] && (settings.owner ? this.files[uploadId].owner != settings.owner : true)) { - reject({status:400,message:"you are not the owner of this file id"}); - return - } + if (!this.uploadId) this.setUploadId(generateFileId()) - if (this.files[uploadId] && this.files[uploadId].reserved) { - reject({status:400,message:"already uploading this file. if your file is stuck in this state, contact an administrator"}); - return - } + let ogf = this.files.db.data[this.uploadId!] - if (settings.name.length > 128) { - reject({status:400,message:"name too long"}); - return - } + this.files.db.data[this.uploadId!] = { + filename: this.name, + mime: this.mime, + messageids: this.messages, + owner: this.owner, + sizeInBytes: this.filled, + visibility: ogf + ? ogf.visibility + : this.owner + && Accounts.getFromId(this.owner)?.defaultFileVisibility + || "public", + // so that json.stringify doesnt include tag:undefined + ...((ogf || {}).tag ? { tag: ogf.tag } : {}), - if (settings.mime.length > 128) { - reject({status:400,message:"mime too long"}); - return - } + chunkSize: this.files.config.maxDiscordFileSize, - // reserve file, hopefully should prevent - // large files breaking + md5: this.hash.digest("hex"), + lastModified: Date.now(), + } - let ogf = this.files[uploadId] + delete this.files.locks[this.uploadId!] + if (ogf?.messageids) await this.files.api.deleteMessages(ogf.messageids) + await this.files.db.save() + if (this.owner) Accounts.files.index(this.owner, this.uploadId!) + return this.uploadId + } - this.files[uploadId] = { - filename:settings.name, - messageids:[], - mime:settings.mime, - sizeInBytes:0, + // exposed methods - owner:settings.owner, - visibility: settings.owner ? "private" : "public", - reserved: true, + setName(name: string) { + if (this.name) + return this.destroy( + new WebError(400, "duplicate attempt to set filename") + ) + if (name.length > 512) + return this.destroy( + new WebError(400, "filename can be a maximum of 512 characters") + ) - chunkSize: this.config.maxDiscordFileSize - } - - // save + this.name = name + return this + } - if (settings.owner) { - await files.index(settings.owner,uploadId) - } - - // get buffer - if (fBuffer.byteLength >= (this.config.maxDiscordFileSize*this.config.maxDiscordFiles)) { - reject({status:400,message:"file too large"}); - return - } - - // generate buffers to upload - let toUpload = [] - for (let i = 0; i < Math.ceil(fBuffer.byteLength/this.config.maxDiscordFileSize); i++) { - toUpload.push( - fBuffer.subarray( - i*this.config.maxDiscordFileSize, - Math.min( - fBuffer.byteLength, - (i+1)*this.config.maxDiscordFileSize - ) - ) + setType(type: string) { + if (this.mime) + return this.destroy( + new WebError(400, "duplicate attempt to set mime type") + ) + if (type.length > 256) + return this.destroy( + new WebError( + 400, + "mime type can be a maximum of 256 characters" ) - } - - // begin uploading - let uploadTmplt:Discord.AttachmentBuilder[] = toUpload.map((e) => { - return new Discord.AttachmentBuilder(e) - .setName(Math.random().toString().slice(2)) - }) - let uploadGroups = [] - for (let i = 0; i < Math.ceil(uploadTmplt.length/10); i++) { - uploadGroups.push(uploadTmplt.slice(i*10,((i+1)*10))) - } - - let msgIds = [] - - for (let i = 0; i < uploadGroups.length; i++) { + ) - let ms = await this.uploadChannel.send({ - files:uploadGroups[i] - }).catch((e) => {console.error(e)}) - - if (ms) { - msgIds.push(ms.id) - } else { - if (!ogf) delete this.files[uploadId] - else this.files[uploadId] = ogf - reject({status:500,message:"please try again"}); return - } - } - - // this code deletes the files from discord, btw - // if need be, replace with job queue system - - if (ogf&&this.uploadChannel) { - for (let x of ogf.messageids) { - this.uploadChannel.messages.delete(x).catch(err => console.error(err)) - } - } - - resolve(await this.writeFile( - uploadId, - { - filename:settings.name, - messageids:msgIds, - mime:settings.mime, - sizeInBytes:fBuffer.byteLength, - - owner:settings.owner, - visibility: ogf ? ogf.visibility - : ( - settings.owner - ? Accounts.getFromId(settings.owner)?.defaultFileVisibility - : undefined - ), - // so that json.stringify doesnt include tag:undefined - ...((ogf||{}).tag ? {tag:ogf.tag} : {}), - - chunkSize: this.config.maxDiscordFileSize - } - )) - - - }) + this.mime = type + return this + } + + setUploadId(id: string) { + if (this.uploadId) + return this.destroy( + new WebError(400, "duplicate attempt to set upload ID") + ) + + let check = schemas.FileId.safeParse(id); + + if (!check.success) + return this.destroy(new WebError(400, issuesToMessage(check.error.issues))) + + if (this.files.db.data[id] && this.files.db.data[id].owner != this.owner) + return this.destroy(new WebError(403, "you don't own this file")) + + if (this.files.locks[id]) + return this.destroy( + new WebError( + 409, + "a file with this ID is already being uploaded" + ) + ) + + this.files.locks[id] = true + this.uploadId = id + return this + } + + // merged StreamBuffer helper + + filled: number = 0 + current?: Readable + messages: string[] = [] + + private newmessage_debounce: boolean = true + + private async startMessage(): Promise { + if (!this.newmessage_debounce) return + this.newmessage_debounce = false + + let wrt = this + + let stream = new Readable({ + read() { + // this is stupid but it should work + console.log( + "Read called; calling on server to execute callback" + ) + wrt.emit("exec-callback") + }, + }) + stream.pause() + + console.log(`Starting a message`) + this.files.api + .send(stream) + .then((message) => { + this.messages.push(message.id) + console.log(`Sent: ${message.id}`) + this.newmessage_debounce = true + this.emit("debounceReleased") + }) + .catch((e) => { + if (!this.errored) this.destroy(e) + }) + + return stream + } + + private async getNextStream() { + console.log("Getting stream...") + console.log("current:" + (this.current ? "yes" : "no")) + if (this.current) return this.current + else if (this.newmessage_debounce) { + // startmessage.... idk + this.current = await this.startMessage() + return this.current + } else { + return new Promise((resolve, reject) => { + console.log("Waiting for debounce to be released...") + this.once("debounceReleased", async () => + resolve(await this.getNextStream()) + ) + }) + } + } +} + +export default class Files { + config: Configuration + api: API + db = new DbFile<{ [key: string]: FilePointer }>("files", {}) + + locks: Record = {} // I'll, like, do something more proper later + + constructor(config: Configuration) { + this.config = config + this.api = new API(config.discordToken!, config) + this.db.read() + } + + createWriteStream(owner?: string) { + return new UploadStream(this, owner) } - - // fs /** - * @description Writes a file to disk - * @param uploadId New file's ID - * @param file FilePointer representing the new file - * @returns Promise which resolves to the file's ID + * @description Update a file from monofile 1.x to 2.x + * @param uploadId Target file's ID */ - writeFile(uploadId: string, file: FilePointer):Promise { - return new Promise((resolve, reject) => { - this.files[uploadId] = file - - writeFile(process.cwd()+"/.data/files.json",JSON.stringify(this.files),(err) => { - - if (err) { - reject({status:500,message:"server may be misconfigured, contact admin for help"}); - delete this.files[uploadId]; - return - } + async update(uploadId: string) { + let target_file = this.db.data[uploadId] + let attachments: APIAttachment[] = [] - resolve(uploadId) - - }) + for (let message of target_file.messageids) { + let attachments = (await this.api.fetchMessage(message)).attachments + for (let attachment of attachments) { + attachments.push(attachment) + } + } - }) + if (!target_file.sizeInBytes) + target_file.sizeInBytes = attachments.reduce( + (a, b) => a + b.size, + 0 + ) + + if (!target_file.chunkSize) target_file.chunkSize = attachments[0].size + + if (!target_file.lastModified) target_file.lastModified = convertSnowflakeToDate(target_file.messageids[target_file.messageids.length-1]).getTime() + + // this feels like needlessly heavy + // we should probably just do this in an actual readFile idk + if (!target_file.md5) { + let hash = crypto.createHash("md5"); + (await this.readFileStream(uploadId)).pipe(hash).once("end", () => target_file.md5 = hash.digest("hex")) + } } /** * @description Read a file * @param uploadId Target file's ID * @param range Byte range to get - * @returns A `Readable` containing the file's contents + * @returns A {@link Readable} containing the file's contents */ - readFileStream(uploadId: string, range?: {start:number, end:number}):Promise { - return new Promise(async (resolve,reject) => { - if (!this.uploadChannel) { - reject({status:503,message:"server is not ready - please try again later"}) - return - } - - if (this.files[uploadId]) { - let file = this.files[uploadId] - - let - scan_msg_begin = 0, - scan_msg_end = file.messageids.length-1, - scan_files_begin = 0, - scan_files_end = -1 - - let useRanges = range && file.chunkSize && file.sizeInBytes; - - // todo: figure out how to get typesccript to accept useRanges - // i'm too tired to look it up or write whatever it wnats me to do - if (range && file.chunkSize && file.sizeInBytes) { - - // Calculate where to start file scans... - - scan_files_begin = Math.floor(range.start / file.chunkSize) - scan_files_end = Math.ceil(range.end / file.chunkSize) - 1 - - scan_msg_begin = Math.floor(scan_files_begin / 10) - scan_msg_end = Math.ceil(scan_files_end / 10) - - } - - let attachments: Discord.Attachment[] = []; - - /* File updates */ - let file_updates: Pick = {} - let atSIB: number[] = [] // kepes track of the size of each file... - - for (let xi = scan_msg_begin; xi < scan_msg_end+1; xi++) { - - let msg = await this.uploadChannel.messages.fetch(file.messageids[xi]).catch(() => {return null}) - if (msg?.attachments) { - - let attach = Array.from(msg.attachments.values()) - for (let i = (useRanges && xi == scan_msg_begin ? ( scan_files_begin - (xi*10) ) : 0); i < (useRanges && xi == scan_msg_end ? ( scan_files_end - (xi*10) + 1 ) : attach.length); i++) { - - attachments.push(attach[i]) - atSIB.push(attach[i].size) - - } - - } - - } - - if (!file.sizeInBytes) file_updates.sizeInBytes = atSIB.reduce((a,b) => a+b, 0); - if (!file.chunkSize) file_updates.chunkSize = atSIB[0] - if (Object.keys(file_updates).length) { // if file_updates not empty - // i gotta do these weird workarounds, ts is weird sometimes - // originally i was gonna do key is keyof FilePointer but for some reason - // it ended up making typeof file[key] never??? so - // its 10pm and chinese people suck at being quiet so i just wanna get this over with - // chinese is the worst language in terms of volume lmao - let valid_fp_keys = ["sizeInBytes", "chunkSize"] - let isValidFilePointerKey = (key: string): key is "sizeInBytes" | "chunkSize" => valid_fp_keys.includes(key) - - for (let [key,value] of Object.entries(file_updates)) { - if (isValidFilePointerKey(key)) file[key] = value - } - - writeFile(process.cwd()+"/.data/files.json",JSON.stringify(this.files),(err) => {}) - } - - let position = 0; - - let getNextChunk = async () => { - let scanning_chunk = attachments[position] - if (!scanning_chunk) { - return null - } - - let d = await axios.get( - scanning_chunk.url, - { - responseType:"arraybuffer", - headers: { - ...(useRanges ? { - "Range": `bytes=${position == 0 && range && file.chunkSize ? range.start-(scan_files_begin*file.chunkSize) : "0"}-${position == attachments.length-1 && range && file.chunkSize ? range.end-(scan_files_end*file.chunkSize) : ""}` - } : {}) - } - } - ).catch((e:Error) => {console.error(e)}) - - position++; - - if (d) { - return d.data - } else { - reject({status:500,message:"internal server error"}) - return "__ERR" - } - } - - let ord:number[] = [] - // hopefully this regulates it? - let lastChunkSent = true - - let dataStream = new Readable({ - read(){ - if (!lastChunkSent) return - lastChunkSent = false - getNextChunk().then(async (nextChunk) => { - if (nextChunk == "__ERR") {this.destroy(new Error("file read error")); return} - let response = this.push(nextChunk) - - if (!nextChunk) return // EOF - - while (response) { - let nextChunk = await getNextChunk() - response = this.push(nextChunk) - if (!nextChunk) return - } - lastChunkSent = true - }) - } - }) - - resolve(dataStream) - - } else { - reject({status:404,message:"not found"}) - } - }) + async readFileStream( + uploadId: string, + range?: { start: number; end: number } + ): Promise { + if (this.db.data[uploadId]) { + let file = this.db.data[uploadId] + if (!file.sizeInBytes || !file.chunkSize) + await this.update(uploadId) + return new ReadStream(this, file, range) + } else { + throw { status: 404, message: "not found" } + } } /** @@ -430,43 +661,61 @@ export default class Files { * @param uploadId Target file's ID * @param noWrite Whether or not the change should be written to disk. Enable for bulk deletes */ - unlink(uploadId:string, noWrite: boolean = false):Promise { - return new Promise(async (resolve,reject) => { - let tmp = this.files[uploadId]; - if (!tmp) {resolve(); return} - if (tmp.owner) { - let id = files.deindex(tmp.owner,uploadId,noWrite); - if (id) await id - } - // this code deletes the files from discord, btw - // if need be, replace with job queue system + async unlink(uploadId: string, noWrite: boolean = false): Promise { + let target = this.db.data[uploadId] + if (!target) return - if (!this.uploadChannel) {reject(); return} - for (let x of tmp.messageids) { - this.uploadChannel.messages.delete(x).catch(err => console.error(err)) - } + await this.api.deleteMessages(target.messageids) - delete this.files[uploadId]; - if (noWrite) {resolve(); return} - writeFile(process.cwd()+"/.data/files.json",JSON.stringify(this.files),(err) => { - if (err) { - this.files[uploadId] = tmp // !! this may not work, since tmp is a link to this.files[uploadId]? - reject() - } else { - resolve() - } - }) + if (target.owner) { + let id = files.deindex(target.owner, uploadId, noWrite) + if (id) await id + } + delete this.db.data[uploadId] - }) + if (!noWrite) + return this.db.save() } - /** - * @description Get a file's FilePointer - * @param uploadId Target file's ID - * @returns FilePointer for the file - */ - getFilePointer(uploadId:string):FilePointer { - return this.files[uploadId] + async chown(uploadId: string, newOwner?: string, noWrite: boolean = false) { + let target = this.db.data[uploadId] + if (target.owner) { + let i = files.deindex(target.owner, uploadId, Boolean(newOwner && noWrite)) + if (i) await i + } + + target.owner = newOwner + if (newOwner) { + let i = files.index(newOwner, uploadId, noWrite) + if (i) await i + } + + if (!noWrite) + return this.db.save() + } + + async mv(uploadId: string, newId: string, noWrite: boolean = false) { + let target = this.db.data[uploadId] + + if (newId in this.db.data) { + throw new Error("overwriting another file with this move") + } + + if (target.owner) { + let owner = Accounts.getFromId(target.owner) + if (owner) { + owner.files.splice(owner.files.indexOf(uploadId), 1, newId) + if (!noWrite) + await Accounts.Db.save() + } + } + + this.db.data[newId] = target + delete this.db.data[uploadId] + + if (!noWrite) + return this.db.save() } -} + apply = apply.bind(null, this) +} \ No newline at end of file diff --git a/src/server/lib/invites.ts b/src/server/lib/invites.ts new file mode 100644 index 0000000..8a6f06c --- /dev/null +++ b/src/server/lib/invites.ts @@ -0,0 +1,35 @@ +// The only reason we have this is to make +// life very, very slightly easier. +// And also we can change how the invite +// system works a little easily +// if need be, I guess? + +import DbFile from "./dbfile.js"; +import { generateFileId } from "./files.js"; + +export const Db = new DbFile("invites", []) + +export function has(id: string) { + return Db.data.includes(id) +} + +export function use(id: string) { + if (!has(id)) return false + + Db.data.splice( + Db.data.indexOf(id), + 1 + ) + + Db.save() + return true +} + +export function make() { + let invite = generateFileId(6) + Db.data.push(invite) + Db.save() + return invite +} + +Db.read() \ No newline at end of file diff --git a/src/server/lib/mail.ts b/src/server/lib/mail.ts index bd9ce07..22c5f8d 100644 --- a/src/server/lib/mail.ts +++ b/src/server/lib/mail.ts @@ -1,23 +1,17 @@ -import { createTransport } from "nodemailer"; +import { createTransport } from "nodemailer" +import config from "./config.js" -// required i guess -require("dotenv").config() - -let -mailConfig = - require( process.cwd() + "/config.json" ).mail, -transport = - createTransport( - { - ...mailConfig.transport, - auth: { - user: process.env.MAIL_USER, - pass: process.env.MAIL_PASS - } - } - ) - -// lazy but +const { mail } = config +const transport = createTransport({ + host: mail.transport.host, + port: mail.transport.port, + secure: mail.transport.secure, + from: mail.send.from, + auth: { + user: mail.user, + pass: mail.pass, + }, +}) /** * @description Sends an email @@ -26,20 +20,20 @@ transport = * @param content Email content * @returns Promise which resolves to the output from nodemailer.transport.sendMail */ -export function sendMail(to: string, subject: string, content: string) { - return new Promise((resolve,reject) => { - transport.sendMail({ - to, - subject, - "from": mailConfig.send.from, - "html": `monofile accounts
Gain control of your uploads.

${ - content - .replace(/\/g, `@`) - .replace(/\/g,``) - }

If you do not believe that you are the intended recipient of this email, please disregard this message.` - }, (err, info) => { - if (err) reject(err) - else resolve(info) - }) +export async function sendMail(to: string, subject: string, content: string) { + if (!config.mail.enabled) return false + + return transport.sendMail({ + to, + subject, + html: `monofile accounts
Gain control of your uploads.

${content + .replaceAll( + "", + `@` + ) + .replaceAll( + "", + `` + )}

If you do not believe that you are the intended recipient of this email, please disregard this message.`, }) } \ No newline at end of file diff --git a/src/server/lib/middleware.ts b/src/server/lib/middleware.ts index 2d312ad..742c6bc 100644 --- a/src/server/lib/middleware.ts +++ b/src/server/lib/middleware.ts @@ -1,36 +1,109 @@ -import * as Accounts from "./accounts"; -import express, { type RequestHandler } from "express" -import ServeError from "../lib/errors"; -import * as auth from "./auth"; +import * as Accounts from "./accounts.js" +import type { Context, Hono, Handler as RequestHandler } from "hono" +import ServeError from "../lib/errors.js" +import * as auth from "./auth.js" +import { setCookie } from "hono/cookie" +import { z } from "zod" +import { codes } from "./codes.js" /** - * @description Middleware which adds an account, if any, to res.locals.acc + * @description Middleware which adds an account, if any, to ctx.get("account") */ -export const getAccount: RequestHandler = function(req, res, next) { - res.locals.acc = Accounts.getFromToken(auth.tokenFor(req)) - next() +export const getAccount: RequestHandler = async function (ctx, next) { + let uToken = (await auth.tokenFor(ctx))! + let account = Accounts.getFromToken(uToken) + if (account?.suspension) + auth.invalidate(uToken) + ctx.set("account", account) + return next() +} + +export function resolveTarget(actor: Accounts.Account, target: Accounts.AccountResolvable) { + return target == "me" + ? actor + : Accounts.resolve(target) } /** - * @description Middleware which blocks requests which do not have res.locals.acc set + * @description use :user param to get a target for this route */ -export const requiresAccount: RequestHandler = function(_req, res, next) { - if (!res.locals.acc) { - ServeError(res, 401, "not logged in") - return - } - next() + +export const getTarget: RequestHandler = async (ctx, next) => { + let tok = await auth.tokenFor(ctx) + let permissions + if (tok && auth.getType(tok) != "User") + permissions = auth.getScopes(tok) + + let actor = ctx.get("account") + let target = resolveTarget(actor, ctx.req.param("user")) + + if (!target) return ServeError(ctx, 404, "account does not exist") + + if (actor && ( + ( + target != actor // target is not the current account + && ( + !actor?.admin // account is not admin + || ( + permissions && !permissions.includes("manage_server") // account is admin but permissions does not include manage_server + ) + ) + ) + )) + return ServeError(ctx, 403, "you cannot manage this user") + + ctx.set("target", target) + + return next() } /** - * @description Middleware which blocks requests that have res.locals.acc.admin set to a falsy value + * @description Blocks routes with a target user set to the account performing the action from bot tokens which do not have the manage_account permission */ -export const requiresAdmin: RequestHandler = function(_req, res, next) { - if (!res.locals.acc.admin) { - ServeError(res, 403, "you are not an administrator") - return +export const accountMgmtRoute: RequestHandler = async (ctx,next) => { + let tok = await auth.tokenFor(ctx) + let permissions + if (tok && auth.getType(tok) != "User") + permissions = auth.getScopes(tok) + + if ( + ( + ctx.get("account") == ctx.get("target") // if the current target is the user account + && (permissions && !permissions.includes("manage_account")) // if permissions does not include manage_account + ) + ) + return ServeError(ctx, 403, "you cannot manage this user") + + return next() +} + +/** + * @description Middleware which blocks requests which do not have ctx.get("account") set + */ +export const requiresAccount: RequestHandler = function (ctx, next) { + if (!ctx.get("account")) + return ServeError(ctx, 401, "not logged in") + return next() +} + +/** + * @description Middleware which blocks requests which do not have ctx.get("target") set + */ +export const requiresTarget: RequestHandler = function (ctx, next) { + if (!ctx.get("target")) { + return ServeError(ctx, 404, "no target account") } - next() + return next() +} + +/** + * @description Middleware which blocks requests that have ctx.get("account").admin set to a falsy value + */ +export const requiresAdmin: RequestHandler = function (ctx, next) { + if (!ctx.get("account").admin) { + return ServeError(ctx, 403, "you are not an administrator") + } + return next() } /** @@ -38,36 +111,115 @@ export const requiresAdmin: RequestHandler = function(_req, res, next) { * @param tokenPermissions Permissions which your route requires. * @returns Express middleware */ - -export const requiresPermissions = function(...tokenPermissions: auth.TokenPermission[]): RequestHandler { - return function(req, res, next) { - let token = auth.tokenFor(req) +export const requiresScopes = function ( + ...wantsScopes: auth.Scope[] +): RequestHandler { + return async function (ctx, next) { + let token = (await auth.tokenFor(ctx))! let type = auth.getType(token) - - if (type == "App") { - let permissions = auth.getPermissions(token) - - if (!permissions) ServeError(res, 403, "insufficient permissions") - else { - for (let v of tokenPermissions) { - if (!permissions.includes(v as auth.TokenPermission)) { - ServeError(res,403,"insufficient permissions") - return + if (type != "User") { + let scopes = auth.getScopes(token) + + if (!scopes) return ServeError(ctx, 403, "insufficient permissions") + else { + for (let v of wantsScopes) { + if (!scopes.includes(v)) { + return ServeError(ctx, 403, "insufficient permissions") } } - next() - } - } else next() + } + + return next() } } /** - * @description Blocks requests based on whether or not the token being used to access the route is of type `User`. + * @description Blocks requests based on whether or not the token being used to access the route is of type `User`. */ -export const noAPIAccess: RequestHandler = function(req, res, next) { - if (auth.getType(auth.tokenFor(req)) == "App") ServeError(res, 403, "apps are not allowed to access this endpoint") - else next() -} \ No newline at end of file +export const noAPIAccess: RequestHandler = async function (ctx, next) { + if (auth.getType((await auth.tokenFor(ctx))!) == "App") + return ServeError(ctx, 403, "apps are not allowed to access this endpoint") + else return next() +} + +/** + @description Add a restriction to this route; the condition must be true to allow API requests. +*/ + +export const assertAPI = function ( + condition: (ctx: Context) => boolean +): RequestHandler { + return async function (ctx, next) { + let reqToken = (await auth.tokenFor(ctx))! + if ( + auth.getType(reqToken) != "User" && + condition(ctx) + ) + return ServeError( + ctx, + 403, + "apps are not allowed to access this endpoint" + ) + else return next() + } +} + +export const issuesToMessage = function(issues: z.ZodIssue[]) { + return issues.map(e => `${e.path}: ${e.code} :: ${e.message}`).join("; ") +} + +export function scheme(scheme: z.ZodTypeAny, transformer: (ctx: Context) => Promise|any = c => c.req.json()): RequestHandler { + return async function(ctx, next) { + let data = transformer(ctx) + let chk = await scheme.safeParse(data instanceof Promise ? await data : data) + ctx.set("parsedScheme", chk.data) + + if (chk.success) + return next() + else + return ServeError(ctx, 400, issuesToMessage(chk.error.issues)) + } +} + +// this is bad but idgaf +export function runtimeEvaluatedScheme(sch: (c: Context) => z.ZodTypeAny, transformer?: Parameters[1]): RequestHandler { + return async function(ctx, next) { + return scheme(sch(ctx),transformer)(ctx, next) + } +} + +// Not really middleware but a utility + +export const login = async (ctx: Context, account: Accounts.AccountResolvable) => { + let token = auth.create(account, 3 * 24 * 60 * 60 * 1000) + setCookie(ctx, "auth", await auth.makeJwt(token), { + path: "/", + sameSite: "Strict", + secure: true, + httpOnly: true + }) + return token +} + +export const verifyPoi = (user: string, poi?: string, wantsMfaPoi: boolean = false) => { + if (!poi) return false + + let poiCode = codes.identityProof.byId.get(poi) + + if (!poiCode || poiCode.for !== user || poiCode.data == wantsMfaPoi) + return false + + poiCode.terminate() + return true +} + +export const mirror = (apiRoot: Hono, ctx: Context, url: string, init: Partial) => apiRoot.fetch( + new Request( + (new URL(url, ctx.req.raw.url)).href, + init.body ? {...ctx.req.raw, headers: ctx.req.raw.headers, ...init} : Object.assign(ctx.req.raw, init) + ), + ctx.env +) \ No newline at end of file diff --git a/src/server/lib/package.ts b/src/server/lib/package.ts new file mode 100644 index 0000000..8ab7a05 --- /dev/null +++ b/src/server/lib/package.ts @@ -0,0 +1,6 @@ +// bad but works for now +import {readFile} from "fs/promises" +export default JSON.parse( + (await readFile("./package.json")) + .toString() +) satisfies { version: string } \ No newline at end of file diff --git a/src/server/lib/ratelimit.ts b/src/server/lib/ratelimit.ts index 94d9d32..d39350f 100644 --- a/src/server/lib/ratelimit.ts +++ b/src/server/lib/ratelimit.ts @@ -1,50 +1,50 @@ -import { RequestHandler } from "express" -import { type Account } from "./accounts" -import ServeError from "./errors" +import type { Handler } from "hono" +import ServeError from "./errors.js" interface RatelimitSettings { - requests: number per: number - } /** - * @description Ratelimits a route based on res.locals.acc + * @description Ratelimits a route based on ctx.get("account") * @param settings Ratelimit settings * @returns Express middleware */ -export function accountRatelimit( settings: RatelimitSettings ): RequestHandler { +export function accountRatelimit(settings: RatelimitSettings): Handler { let activeLimits: { - [ key: string ]: { - requests: number, + [key: string]: { + requests: number expirationHold: NodeJS.Timeout } } = {} - return (req, res, next) => { - if (res.locals.acc) { - let accId = res.locals.acc.id + return (ctx, next) => { + if (ctx.get("account")) { + let accId = ctx.get("account").id let aL = activeLimits[accId] - + if (!aL) { activeLimits[accId] = { requests: 0, - expirationHold: setTimeout(() => delete activeLimits[accId], settings.per) + expirationHold: setTimeout( + () => delete activeLimits[accId], + settings.per + ), } aL = activeLimits[accId] } if (aL.requests < settings.requests) { - res.locals.undoCount = () => { + ctx.set("undoCount", () => { if (activeLimits[accId]) { activeLimits[accId].requests-- } - } - next() + }) + return next() } else { - ServeError(res, 429, "too many requests") + return ServeError(ctx, 429, "too many requests") } } } -} \ No newline at end of file +} diff --git a/src/server/lib/schemas/accounts.ts b/src/server/lib/schemas/accounts.ts new file mode 100644 index 0000000..64052b6 --- /dev/null +++ b/src/server/lib/schemas/accounts.ts @@ -0,0 +1,74 @@ +import {z} from "zod" +import { FileId, FileVisibility } from "./files.js" +import { RGBHex } from "./misc.js" + +export const StringPassword = z.string().min(8,"password must be at least 8 characters") +export const Password = + z.object({ + hash: z.string(), + salt: z.string() + }) +export const Username = + z.string() + .min(3, "username too short") + .max(20, "username too long") + .regex(/^[A-Za-z0-9_\-\.]+$/, "username contains invalid characters") + +export namespace Settings { + export const Theme = z.discriminatedUnion("theme", [ + z.object({ + theme: z.literal("catppuccin"), + variant: z.enum(["latte","frappe","macchiato","mocha","adaptive"]), + accent: z.enum([ + "rosewater", + "flamingo", + "pink", + "mauve", + "red", + "maroon", + "peach", + "yellow", + "green", + "teal", + "sky", + "sapphire", + "blue", + "lavender" + ]) + }), + z.object({ + theme: z.literal("custom"), + id: FileId + }) + ]) + export const BarSide = z.enum(["top","left","bottom","right"]) + export const Interface = z.object({ + theme: Theme.default({theme: "catppuccin", variant: "adaptive", accent: "sky"}), + barSide: BarSide.default("left") + }) + export const Links = z.object({ + color: RGBHex.optional(), + largeImage: z.boolean().default(false) + }) + export const User = z.object({ + interface: Interface.default({}), links: Links.default({}) + }) +} +export const Suspension = + z.object({ + reason: z.string(), + until: z.number().nullable() + }) +export const Account = + z.object({ + id: z.string(), + username: Username, + email: z.optional(z.string().email("must be an email")), + password: Password, + files: z.array(z.string()), + admin: z.boolean(), + defaultFileVisibility: FileVisibility, + + settings: Settings.User, + suspension: Suspension.optional() + }) \ No newline at end of file diff --git a/src/server/lib/schemas/auth.ts b/src/server/lib/schemas/auth.ts new file mode 100644 index 0000000..3e584bf --- /dev/null +++ b/src/server/lib/schemas/auth.ts @@ -0,0 +1,40 @@ +import { z } from "zod"; + +export const Scope = z.enum([ + "user", // permissions to /auth/me, with email docked + "email", // adds email back to /auth/me + "private", // allows app to manage and read private files + "manage_files", // allows an app to manage an account's files + "manage_account", // allows an app to manage an account + "manage_server" // allows an app to affect other users, files on admin accounts +]) + +export const TokenType = z.enum([ + "User", + "ApiKey", + "App" +]) + +const BaseAuthToken = z.object({ + account: z.string(), + id: z.string(), + expire: z.number() + .nullable() + .refine(e => e == null || e > Date.now(), "expiration must be after now"), + + type: TokenType +}) + +export const AuthToken = z.discriminatedUnion("type",[ + BaseAuthToken.extend({ + type: z.literal("User") + }), + BaseAuthToken.extend({ + type: z.literal("ApiKey"), + scopes: z.array(Scope).default(["user"]) + }), + BaseAuthToken.extend({ + type: z.literal("App"), + scopes: z.array(Scope).default(["user"]) + }) +]) \ No newline at end of file diff --git a/src/server/lib/schemas/files.ts b/src/server/lib/schemas/files.ts new file mode 100644 index 0000000..2c09613 --- /dev/null +++ b/src/server/lib/schemas/files.ts @@ -0,0 +1,21 @@ +import {z} from "zod" +import config from "../config.js" + +export const FileId = z.string() + .regex(/^[A-Za-z0-9_\-\.\!\=\:\&\$\,\+\;\@\~\*\(\)\']+$/,"file ID uses invalid characters") + .max(config.maxUploadIdLength,"file ID too long") + .min(1, "you... *need* a file ID") +export const FileVisibility = z.enum(["public", "anonymous", "private"]) +export const FileTag = z.string().toLowerCase().regex(/^[a-z\-]+$/, "invalid characters").max(30, "tag length too long") +export const FilePointer = z.object({ + filename: z.string().max(512, "filename too long"), + mime: z.string().max(256, "mimetype too long"), + messageids: z.array(z.string()), + owner: z.optional(z.string()), + sizeInBytes: z.optional(z.number()), + tag: z.optional(FileTag.array().max(5)), + visibility: z.optional(FileVisibility).default("public"), + chunkSize: z.optional(z.number()), + lastModified: z.optional(z.number()), + md5: z.optional(z.string()) +}) \ No newline at end of file diff --git a/src/server/lib/schemas/index.ts b/src/server/lib/schemas/index.ts new file mode 100644 index 0000000..5689486 --- /dev/null +++ b/src/server/lib/schemas/index.ts @@ -0,0 +1,3 @@ +export * as AccountSchemas from "./accounts.js" +export * as FileSchemas from "./files.js" +export * as AuthSchemas from "./auth.js" \ No newline at end of file diff --git a/src/server/lib/schemas/misc.ts b/src/server/lib/schemas/misc.ts new file mode 100644 index 0000000..7eea94d --- /dev/null +++ b/src/server/lib/schemas/misc.ts @@ -0,0 +1,3 @@ +import { z } from "zod"; + +export const RGBHex = z.string().toLowerCase().length(6).regex(/^[a-f0-9]+$/,"illegal characters") \ No newline at end of file diff --git a/src/server/routes/adminRoutes.ts b/src/server/routes/adminRoutes.ts deleted file mode 100644 index 510eec2..0000000 --- a/src/server/routes/adminRoutes.ts +++ /dev/null @@ -1,235 +0,0 @@ -import bodyParser from "body-parser"; -import { Router } from "express"; -import * as Accounts from "../lib/accounts"; -import * as auth from "../lib/auth"; -import bytes from "bytes" -import {writeFile} from "fs"; -import { sendMail } from "../lib/mail"; -import { getAccount, requiresAccount, requiresAdmin, requiresPermissions } from "../lib/middleware" - -import ServeError from "../lib/errors"; -import Files from "../lib/files"; - -let parser = bodyParser.json({ - type: ["text/plain","application/json"] -}) - -export let adminRoutes = Router(); -adminRoutes - .use(getAccount) - .use(requiresAccount) - .use(requiresAdmin) - .use(requiresPermissions("admin")) -let files:Files - -export function setFilesObj(newFiles:Files) { - files = newFiles -} - -let config = require(`${process.cwd()}/config.json`) - -adminRoutes.post("/reset", parser, (req,res) => { - - let acc = res.locals.acc as Accounts.Account - - if (typeof req.body.target !== "string" || typeof req.body.password !== "string") { - res.status(404) - res.send() - return - } - - let targetAccount = Accounts.getFromUsername(req.body.target) - if (!targetAccount) { - res.status(404) - res.send() - return - } - - Accounts.password.set ( targetAccount.id, req.body.password ) - auth.AuthTokens.filter(e => e.account == targetAccount?.id).forEach((v) => { - auth.invalidate(v.token) - }) - - if (targetAccount.email) { - sendMail(targetAccount.email, `Your login details have been updated`, `Hello there! This email is to notify you of a password change that an administrator, ${acc.username}, has initiated. You have been logged out of your devices. Thank you for using monofile.`).then(() => { - res.send("OK") - }).catch((err) => {}) - } - - - res.send() - -}) - -adminRoutes.post("/elevate", parser, (req,res) => { - - let acc = res.locals.acc as Accounts.Account - - if (typeof req.body.target !== "string") { - res.status(404) - res.send() - return - } - - let targetAccount = Accounts.getFromUsername(req.body.target) - if (!targetAccount) { - res.status(404) - res.send() - return - } - - targetAccount.admin = true; - Accounts.save() - res.send() - -}) - -adminRoutes.post("/delete", parser, (req,res) => { - - if (typeof req.body.target !== "string") { - res.status(404) - res.send() - return - } - - let targetFile = files.getFilePointer(req.body.target) - - if (!targetFile) { - res.status(404) - res.send() - return - } - - files.unlink(req.body.target).then(() => { - res.status(200) - }).catch(() => { - res.status(500) - }).finally(() => res.send()) - -}) - -adminRoutes.post("/delete_account", parser, async (req,res) => { - - let acc = res.locals.acc as Accounts.Account - - if (typeof req.body.target !== "string") { - res.status(404) - res.send() - return - } - - let targetAccount = Accounts.getFromUsername(req.body.target) - if (!targetAccount) { - res.status(404) - res.send() - return - } - - let accId = targetAccount.id - - auth.AuthTokens.filter(e => e.account == accId).forEach((v) => { - auth.invalidate(v.token) - }) - - let cpl = () => Accounts.deleteAccount(accId).then(_ => { - if (targetAccount?.email) { - sendMail(targetAccount.email, "Notice of account deletion", `Your account, ${targetAccount.username}, has been deleted by ${acc.username} for the following reason:

${req.body.reason || "(no reason specified)"}

Your files ${req.body.deleteFiles ? "have been deleted" : "have not been modified"}. Thank you for using monofile.`) - } - res.send("account deleted") - }) - - if (req.body.deleteFiles) { - let f = targetAccount.files.map(e=>e) // make shallow copy so that iterating over it doesnt Die - for (let v of f) { - files.unlink(v,true).catch(err => console.error(err)) - } - - writeFile(process.cwd()+"/.data/files.json",JSON.stringify(files.files), (err) => { - if (err) console.log(err) - cpl() - }) - } else cpl() -}) - -adminRoutes.post("/transfer", parser, (req,res) => { - - if (typeof req.body.target !== "string" || typeof req.body.owner !== "string") { - res.status(404) - res.send() - return - } - - let targetFile = files.getFilePointer(req.body.target) - if (!targetFile) { - res.status(404) - res.send() - return - } - - let newOwner = Accounts.getFromUsername(req.body.owner || "") - - // clear old owner - - if (targetFile.owner) { - let oldOwner = Accounts.getFromId(targetFile.owner) - if (oldOwner) { - Accounts.files.deindex(oldOwner.id, req.body.target) - } - } - - if (newOwner) { - Accounts.files.index(newOwner.id, req.body.target) - } - targetFile.owner = newOwner ? newOwner.id : undefined; - - files.writeFile(req.body.target, targetFile).then(() => { - res.send() - }).catch(() => { - res.status(500) - res.send() - }) // wasting a reassignment but whatee - -}) - -adminRoutes.post("/idchange", parser, (req,res) => { - - if (typeof req.body.target !== "string" || typeof req.body.new !== "string") { - res.status(400) - res.send() - return - } - - let targetFile = files.getFilePointer(req.body.target) - if (!targetFile) { - res.status(404) - res.send() - return - } - - if (files.getFilePointer(req.body.new)) { - res.status(400) - res.send() - return - } - - if (targetFile.owner) { - Accounts.files.deindex(targetFile.owner, req.body.target) - Accounts.files.index(targetFile.owner, req.body.new) - } - delete files.files[req.body.target] - - files.writeFile(req.body.new, targetFile).then(() => { - res.send() - }).catch(() => { - files.files[req.body.target] = req.body.new - - if (targetFile.owner) { - Accounts.files.deindex(targetFile.owner, req.body.new) - Accounts.files.index(targetFile.owner, req.body.target) - } - - res.status(500) - res.send() - }) - -}) \ No newline at end of file diff --git a/src/server/routes/api.ts b/src/server/routes/api.ts new file mode 100644 index 0000000..cfba678 --- /dev/null +++ b/src/server/routes/api.ts @@ -0,0 +1,82 @@ +import { Hono } from "hono" +import Files from "../lib/files.js" +import { fileURLToPath } from "url" +import { dirname } from "path" +import { readdir } from "fs/promises" + +const APIDirectory = dirname(fileURLToPath(import.meta.url)) + "/api" + +interface APIMount { + file: string + to: string +} + +type APIMountResolvable = string | APIMount + +export interface APIDefinition { + name: string + baseURL: string + mount: APIMountResolvable[] +} + +function resolveMount(mount: APIMountResolvable): APIMount { + return typeof mount == "string" ? { file: mount, to: "/" + mount } : mount +} + +class APIVersion { + readonly definition: APIDefinition + readonly apiPath: string + readonly apiRoot: Hono + readonly root: Hono = new Hono() + readonly files: Files + + constructor(definition: APIDefinition, files: Files, apiRoot: Hono) { + this.definition = definition + this.apiPath = APIDirectory + "/" + definition.name + this.files = files + this.apiRoot = apiRoot + } + + async load() { + for (let _mount of this.definition.mount) { + let mount = resolveMount(_mount) + // no idea if there's a better way to do this but this is all i can think of + let { default: route } = (await import( + `${this.apiPath}/${mount.file}.js` + )) as { default: (files: Files, apiRoot: Hono) => Hono } + + this.root.route(mount.to, route(this.files, this.apiRoot)) + } + } +} + +export default class APIRouter { + readonly files: Files + readonly root: Hono = new Hono() + + constructor(files: Files) { + this.files = files + } + + /** + * @description Mounts an APIDefinition to the APIRouter. + * @param definition Definition to mount. + */ + + private async mount(definition: APIDefinition) { + console.log(`mounting APIDefinition ${definition.name}`) + + let def = new APIVersion(definition, this.files, this.root) + await def.load() + + this.root.route(definition.baseURL, def.root) + } + + async loadAPIMethods() { + let files = await readdir(APIDirectory) + for (let version of files) { + let def = (await import(`${APIDirectory}/${version}/definition.js`)).default + await this.mount(def) + } + } +} diff --git a/src/server/routes/api/v0/adminRoutes.ts b/src/server/routes/api/v0/adminRoutes.ts new file mode 100644 index 0000000..1a586b1 --- /dev/null +++ b/src/server/routes/api/v0/adminRoutes.ts @@ -0,0 +1,219 @@ +import { Hono } from "hono" +import * as Accounts from "../../../lib/accounts.js" +import * as auth from "../../../lib/auth.js" +import { writeFile } from "fs/promises" +import { sendMail } from "../../../lib/mail.js" +import { + getAccount, + requiresAccount, + requiresAdmin, + requiresScopes, +} from "../../../lib/middleware.js" +import Files from "../../../lib/files.js" + +export let adminRoutes = new Hono<{ + Variables: { + account: Accounts.Account + } +}>() +adminRoutes + .use(getAccount) + .use(requiresAccount) + .use(requiresAdmin) + .use(requiresScopes("manage_server")) + +export default function (files: Files) { + adminRoutes.post("/reset", async (ctx) => { + let acc = ctx.get("account") as Accounts.Account + const body = await ctx.req.json() + + if ( + typeof body.target !== "string" || + typeof body.password !== "string" + ) { + return ctx.text("not found", 404) + } + + let targetAccount = Accounts.getFromUsername(body.target) + if (!targetAccount) { + return ctx.text("not found", 404) + } + + Accounts.password.set(targetAccount.id, body.password) + auth.Db.data.filter((e) => e.account == targetAccount?.id).forEach( + (v) => { + auth.invalidate(v.id) + } + ) + + if (targetAccount.email) { + return sendMail( + targetAccount.email, + `Your login details have been updated`, + `Hello there! This email is to notify you of a password change that an administrator, ${acc.username}, has initiated. You have been logged out of your devices. Thank you for using monofile.` + ) + .then(() => ctx.text("OK")) + .catch(() => ctx.text("err while sending email", 500)) + } + }) + + adminRoutes.post("/elevate", async (ctx) => { + const body = await ctx.req.json() + let acc = ctx.get("account") as Accounts.Account + + if (typeof body.target !== "string") { + return ctx.text("not found", 404) + } + + let targetAccount = Accounts.getFromUsername(body.target) + if (!targetAccount) { + return ctx.text("not found", 404) + } + + Accounts.Db.save() + return ctx.text("OK") + }) + + adminRoutes.post("/delete", async (ctx) => { + const body = await ctx.req.json() + if (typeof body.target !== "string") { + return ctx.text("not found", 404) + } + + let targetFile = files.db.data[body.target] + + if (!targetFile) { + return ctx.text("not found", 404) + } + + return files + .unlink(body.target) + .then(() => ctx.text("ok", 200)) + .catch(() => ctx.text("err", 500)) + }) + + adminRoutes.post("/delete_account", async (ctx) => { + let acc = ctx.get("account") as Accounts.Account + const body = await ctx.req.json() + if (typeof body.target !== "string") { + return ctx.text("not found", 404) + } + + let targetAccount = Accounts.getFromUsername(body.target) + if (!targetAccount) { + return ctx.text("not found", 404) + } + + let accId = targetAccount.id + + auth.Db.data.filter((e) => e.account == accId).forEach((v) => { + auth.invalidate(v.id) + }) + + let cpl = () => + Accounts.deleteAccount(accId).then((_) => { + if (targetAccount?.email) { + sendMail( + targetAccount.email, + "Notice of account deletion", + `Your account, ${ + targetAccount.username + }, has been deleted by ${ + acc.username + } for the following reason:

${ + body.reason || "(no reason specified)" + }

Your files ${ + body.deleteFiles + ? "have been deleted" + : "have not been modified" + }. Thank you for using monofile.` + ) + } + return ctx.text("account deleted") + }) + + if (body.deleteFiles) { + let f = targetAccount.files.map((e) => e) // make shallow copy so that iterating over it doesnt Die + for (let v of f) { + files.unlink(v, true).catch((err) => console.error(err)) + } + + return writeFile( + process.cwd() + "/.data/files.json", + JSON.stringify(files.db.data) + ).then(cpl) + } else return cpl() + }) + + adminRoutes.post("/transfer", async (ctx) => { + const body = await ctx.req.json() + if (typeof body.target !== "string" || typeof body.owner !== "string") { + return ctx.text("not found", 404) + } + + let targetFile = files.db.data[body.target] + if (!targetFile) { + return ctx.text("not found", 404) + } + + let newOwner = Accounts.getFromUsername(body.owner || "") + + // clear old owner + + if (targetFile.owner) { + let oldOwner = Accounts.getFromId(targetFile.owner) + if (oldOwner) { + Accounts.files.deindex(oldOwner.id, body.target) + } + } + + if (newOwner) { + Accounts.files.index(newOwner.id, body.target) + } + targetFile.owner = newOwner ? newOwner.id : undefined + + return files.db + .save() + .then(() => ctx.text("ok", 200)) + .catch(() => ctx.text("error", 500)) + }) + + adminRoutes.post("/idchange", async (ctx) => { + const body = await ctx.req.json() + if (typeof body.target !== "string" || typeof body.new !== "string") { + return ctx.text("inappropriate body", 400) + } + + let targetFile = files.db.data[body.target] + if (!targetFile) { + return ctx.text("not found", 404) + } + + if (files.db.data[body.new]) { + return ctx.status(400) + } + + if (targetFile.owner) { + Accounts.files.deindex(targetFile.owner, body.target) + Accounts.files.index(targetFile.owner, body.new) + } + delete files.db.data[body.target] + files.db.data[body.new] = targetFile + + return files.db + .save() + .then(() => ctx.status(200)) + .catch(() => { + files.db.data[body.target] = body.new + + if (targetFile.owner) { + Accounts.files.deindex(targetFile.owner, body.new) + Accounts.files.index(targetFile.owner, body.target) + } + + return ctx.status(500) + }) + }) + + return adminRoutes +} diff --git a/src/server/routes/api/v0/authRoutes.ts b/src/server/routes/api/v0/authRoutes.ts new file mode 100644 index 0000000..8ba544a --- /dev/null +++ b/src/server/routes/api/v0/authRoutes.ts @@ -0,0 +1,549 @@ +import { Hono, Handler } from "hono" +import { getCookie, setCookie } from "hono/cookie" +import * as Accounts from "../../../lib/accounts.js" +import * as auth from "../../../lib/auth.js" +import { sendMail } from "../../../lib/mail.js" +import { + getAccount, + login, + noAPIAccess, + requiresAccount, + requiresScopes, +} from "../../../lib/middleware.js" +import { accountRatelimit } from "../../../lib/ratelimit.js" +import config from "../../../lib/config.js" +import ServeError from "../../../lib/errors.js" +import Files, { + FileVisibility, + generateFileId +} from "../../../lib/files.js" + +import { writeFile } from "fs/promises" + +export let authRoutes = new Hono<{ + Variables: { + account: Accounts.Account + } +}>() + +authRoutes.all("*", getAccount) + +export default function (files: Files) { + authRoutes.post("/login", async (ctx) => { + const body = await ctx.req.json() + if ( + typeof body.username != "string" || + typeof body.password != "string" + ) { + return ServeError(ctx, 400, "please provide a username or password") + } + + if (auth.validate(getCookie(ctx, "auth")!)) + return ctx.text("You are already authed") + + /* + check if account exists + */ + + let acc = Accounts.getFromUsername(body.username) + + if (!acc) { + return ServeError(ctx, 401, "username or password incorrect") + } + + if (!Accounts.password.check(acc.id, body.password)) { + return ServeError(ctx, 401, "username or password incorrect") + } + + /* + assign token + */ + + login(ctx, acc.id) + return ctx.text("") + }) + + authRoutes.post("/create", async (ctx) => { + if (!config.accounts.registrationEnabled) { + return ServeError(ctx, 403, "account registration disabled") + } + + if (auth.validate(getCookie(ctx, "auth")!)) return + const body = await ctx.req.json() + if ( + typeof body.username != "string" || + typeof body.password != "string" + ) { + return ServeError(ctx, 400, "please provide a username or password") + } + + /* + check if account exists + */ + + let acc = Accounts.getFromUsername(body.username) + + if (acc) { + return ServeError(ctx, 400, "account with this username already exists") + } + + if (body.username.length < 3 || body.username.length > 20) { + return ServeError( + ctx, + 400, + "username must be over or equal to 3 characters or under or equal to 20 characters in length" + ) + } + + if ( + (body.username.match(/[A-Za-z0-9_\-\.]+/) || [])[0] != body.username + ) { + return ServeError(ctx, 400, "username contains invalid characters") + } + + if (body.password.length < 8) { + return ServeError(ctx, 400, "password must be 8 characters or longer") + } + + return Accounts.create(body.username, body.password) + .then((newAcc) => { + /* + assign token + */ + + login(ctx, newAcc) + return ctx.text("") + }) + .catch(() => ServeError(ctx, 500, "internal server error")) + }) + + authRoutes.post("/logout", async (ctx) => { + if (!auth.validate(getCookie(ctx, "auth")!)) { + return ServeError(ctx, 401, "not logged in") + } + + auth.invalidate(getCookie(ctx, "auth")!) + return ctx.text("logged out") + }) + + authRoutes.post( + "/dfv", + requiresAccount, + requiresScopes("manage_files"), + // Used body-parser + async (ctx) => { + const body = await ctx.req.json() + let acc = ctx.get("account") as Accounts.Account + + if ( + ["public", "private", "anonymous"].includes( + body.defaultFileVisibility + ) + ) { + acc.defaultFileVisibility = body.defaultFileVisibility + Accounts.Db.save() + return ctx.text( + `dfv has been set to ${acc.defaultFileVisibility}` + ) + } else { + return ctx.text("invalid dfv", 400) + } + } + ) + + authRoutes.post( + "/delete_account", + requiresAccount, + noAPIAccess, + // Used body-parser + async (ctx) => { + let acc = ctx.get("account") as Accounts.Account + const body = await ctx.req.json() + let accId = acc.id + + auth.Db.data.filter((e) => e.account == accId).forEach((v) => { + auth.invalidate(v.id) + }) + + let cpl = () => + Accounts.deleteAccount(accId).then((_) => + ctx.text("account deleted") + ) + + if (body.deleteFiles) { + let f = acc.files.map((e) => e) // make shallow copy so that iterating over it doesnt Die + for (let v of f) { + files.unlink(v, true).catch((err) => console.error(err)) + } + + return writeFile( + process.cwd() + "/.data/files.json", + JSON.stringify(files.db.data) + ).then(cpl) + } else cpl() + } + ) + + authRoutes.post( + "/change_username", + requiresAccount, + noAPIAccess, + // Used body-parser + async (ctx) => { + let acc = ctx.get("account") as Accounts.Account + const body = await ctx.req.json() + if ( + typeof body.username != "string" || + body.username.length < 3 || + body.username.length > 20 + ) { + return ServeError( + ctx, + 400, + "username must be between 3 and 20 characters in length" + ) + } + + let _acc = Accounts.getFromUsername(body.username) + + if (_acc) { + return ServeError( + ctx, + 400, + "account with this username already exists" + ) + } + + if ( + (body.username.match(/[A-Za-z0-9_\-\.]+/) || [])[0] != + body.username + ) { + return ServeError( + ctx, + 400, + "username contains invalid characters" + ) + } + + acc.username = body.username + Accounts.Db.save() + + if (acc.email) { + return sendMail( + acc.email, + `Your login details have been updated`, + `Hello there! Your username has been updated to ${body.username}. Please update your devices accordingly. Thank you for using monofile.` + ) + .then(() => ctx.text("OK")) + .catch((err) => {}) + } + + return ctx.text("username changed") + } + ) + + // shit way to do this but... + + let verificationCodes = new Map< + string, + { code: string; email: string; expiry: NodeJS.Timeout } + >() + + authRoutes.post( + "/request_email_change", + requiresAccount, + noAPIAccess, + accountRatelimit({ requests: 4, per: 60 * 60 * 1000 }), + // Used body-parser + async (ctx) => { + let acc = ctx.get("account") as Accounts.Account + const body = await ctx.req.json() + if (typeof body.email != "string" || !body.email) { + ServeError(ctx, 400, "supply an email") + return + } + + let vcode = verificationCodes.get(acc.id) + + // delete previous if any + let e = vcode?.expiry + if (e) clearTimeout(e) + verificationCodes.delete(acc?.id || "") + + let code = generateFileId(12).toUpperCase() + + // set + + verificationCodes.set(acc.id, { + code, + email: body.email, + expiry: setTimeout( + () => verificationCodes.delete(acc?.id || ""), + 15 * 60 * 1000 + ), + }) + + // this is a mess but it's fine + + sendMail( + body.email, + `Hey there, ${acc.username} - let's connect your email`, + `Hello there! You are recieving this message because you decided to link your email, ${ + body.email.split("@")[0] + }@${ + body.email.split("@")[1] + }, to your account, ${ + acc.username + }. If you would like to continue, please click here, or go to https://${ctx.req.header( + "Host" + )}/auth/confirm_email/${code}.` + ) + .then(() => ctx.text("OK")) + .catch((err) => { + let e = verificationCodes.get(acc?.id || "")?.expiry + if (e) clearTimeout(e) + verificationCodes.delete(acc?.id || "") + ;(ctx.get("undoCount" as never) as () => {})() + return ServeError(ctx, 500, err?.toString()) + }) + } + ) + + authRoutes.get( + "/confirm_email/:code", + requiresAccount, + noAPIAccess, + async (ctx) => { + let acc = ctx.get("account") as Accounts.Account + + let vcode = verificationCodes.get(acc.id) + + if (!vcode) { + ServeError(ctx, 400, "nothing to confirm") + return + } + + if ( + typeof ctx.req.param("code") == "string" && + ctx.req.param("code").toUpperCase() == vcode.code + ) { + acc.email = vcode.email + Accounts.Db.save() + + let e = verificationCodes.get(acc?.id || "")?.expiry + if (e) clearTimeout(e) + verificationCodes.delete(acc?.id || "") + + return ctx.redirect("/") + } else { + return ServeError(ctx, 400, "invalid code") + } + } + ) + + authRoutes.post( + "/remove_email", + requiresAccount, + noAPIAccess, + async (ctx) => { + let acc = ctx.get("account") as Accounts.Account + + if (acc.email) { + delete acc.email + Accounts.Db.save() + return ctx.text("email detached") + } else return ServeError(ctx, 400, "email not attached") + } + ) + + let pwReset = new Map< + string, + { code: string; expiry: NodeJS.Timeout; requestedAt: number } + >() + let prcIdx = new Map() + + authRoutes.post("/request_emergency_login", async (ctx) => { + if (auth.validate(getCookie(ctx, "auth") || "")) return + const body = await ctx.req.json() + if (typeof body.account != "string" || !body.account) { + ServeError(ctx, 400, "supply a username") + return + } + + let acc = Accounts.getFromUsername(body.account) + if (!acc || !acc.email) { + return ServeError( + ctx, + 400, + "this account either does not exist or does not have an email attached; please contact the server's admin for a reset if you would still like to access it" + ) + } + + let pResetCode = pwReset.get(acc.id) + + if ( + pResetCode && + pResetCode.requestedAt + 15 * 60 * 1000 > Date.now() + ) { + return ServeError( + ctx, + 429, + `Please wait a few moments to request another emergency login.` + ) + } + + // delete previous if any + let e = pResetCode?.expiry + if (e) clearTimeout(e) + pwReset.delete(acc?.id || "") + prcIdx.delete(pResetCode?.code || "") + + let code = generateFileId(12).toUpperCase() + + // set + + pwReset.set(acc.id, { + code, + expiry: setTimeout( + () => { + pwReset.delete(acc?.id || "") + prcIdx.delete(pResetCode?.code || "") + }, + 15 * 60 * 1000 + ), + requestedAt: Date.now(), + }) + + prcIdx.set(code, acc.id) + + // this is a mess but it's fine + + return sendMail( + acc.email, + `Emergency login requested for ${acc.username}`, + `Hello there! You are recieving this message because you forgot your password to your monofile account, ${ + acc.username + }. To log in, please click here, or go to https://${ctx.req.header( + "Host" + )}/auth/emergency_login/${code}. If it doesn't appear that you are logged in after visiting this link, please try refreshing. Once you have successfully logged in, you may reset your password.` + ) + .then(() => ctx.text("OK")) + .catch((err) => { + let e = pwReset.get(acc?.id || "")?.expiry + if (e) clearTimeout(e) + pwReset.delete(acc?.id || "") + prcIdx.delete(code || "") + return ServeError(ctx, 500, err?.toString()) + }) + }) + + authRoutes.get("/emergency_login/:code", async (ctx) => { + if (auth.validate(getCookie(ctx, "auth") || "")) { + return ServeError(ctx, 403, "already logged in") + } + + let vcode = prcIdx.get(ctx.req.param("code")) + + if (!vcode) { + return ServeError(ctx, 400, "invalid emergency login code") + } + + if (typeof ctx.req.param("code") == "string" && vcode) { + login(ctx, vcode) + let e = pwReset.get(vcode)?.expiry + if (e) clearTimeout(e) + pwReset.delete(vcode) + prcIdx.delete(ctx.req.param("code")) + return ctx.redirect("/") + } else { + ServeError(ctx, 400, "invalid code") + } + }) + + authRoutes.post( + "/change_password", + requiresAccount, + noAPIAccess, + // Used body-parser + async (ctx) => { + let acc = ctx.get("account") as Accounts.Account + const body = await ctx.req.json() + if (typeof body.password != "string" || body.password.length < 8) { + ServeError(ctx, 400, "password must be 8 characters or longer") + return + } + + let accId = acc.id + + Accounts.password.set(accId, body.password) + + auth.Db.data.filter((e) => e.account == accId).forEach((v) => { + auth.invalidate(v.id) + }) + + if (acc.email) { + return sendMail( + acc.email, + `Your login details have been updated`, + `Hello there! This email is to notify you of a password change that you have initiated. You have been logged out of your devices. Thank you for using monofile.` + ) + .then(() => ctx.text("OK")) + .catch((err) => {}) + } + + return ctx.text("password changed - logged out all sessions") + } + ) + + authRoutes.post( + "/logout_sessions", + requiresAccount, + noAPIAccess, + async (ctx) => { + let acc = ctx.get("account") as Accounts.Account + + let accId = acc.id + + auth.Db.data.filter((e) => e.account == accId).forEach((v) => { + auth.invalidate(v.id) + }) + + return ctx.text("logged out all sessions") + } + ) + + authRoutes.get( + "/me", + requiresAccount, + requiresScopes("user"), + async (ctx) => { + let acc = ctx.get("account") as Accounts.Account + let sessionToken = (await auth.tokenFor(ctx))! + let accId = acc.id + return ctx.json({ + ...acc, + sessionCount: auth.Db.data.filter( + (e) => + e.type == "User" && + e.account == accId && + (e.expire == null || e.expire > Date.now()) + ).length, + sessionExpires: auth.Db.data.find( + (e) => e.id == sessionToken + )?.expire, + password: undefined, + email: + auth.getType(sessionToken) == "User" || + auth.getScopes(sessionToken)?.includes("email") + ? acc.email + : undefined, + }) + } + ) + + return authRoutes +} diff --git a/src/server/routes/api/v0/definition.ts b/src/server/routes/api/v0/definition.ts new file mode 100644 index 0000000..6290ac8 --- /dev/null +++ b/src/server/routes/api/v0/definition.ts @@ -0,0 +1,12 @@ +import { APIDefinition } from "../../api.js"; + +export default { + "name": "v0", + "baseURL": "/", + "mount": [ + { "file": "primaryApi", "to": "/" }, + { "file": "adminRoutes", "to": "/admin" }, + { "file": "authRoutes", "to": "/auth" }, + { "file": "fileApiRoutes", "to": "/files" } + ] +} satisfies APIDefinition \ No newline at end of file diff --git a/src/server/routes/api/v0/fileApiRoutes.ts b/src/server/routes/api/v0/fileApiRoutes.ts new file mode 100644 index 0000000..fc2976d --- /dev/null +++ b/src/server/routes/api/v0/fileApiRoutes.ts @@ -0,0 +1,114 @@ +import { Hono } from "hono" +import * as Accounts from "../../../lib/accounts.js" +import { writeFile } from "fs/promises" +import Files from "../../../lib/files.js" +import { + getAccount, + requiresAccount, + requiresScopes, +} from "../../../lib/middleware.js" + +export let fileApiRoutes = new Hono<{ + Variables: { + account: Accounts.Account + } +}>() + +fileApiRoutes.use("*", getAccount) + +export default function (files: Files) { + fileApiRoutes.get( + "/list", + requiresAccount, + requiresScopes("user"), + async (ctx) => { + let acc = ctx.get("account") as Accounts.Account + + if (!acc) return + let accId = acc.id + + return ctx.json( + acc.files + .map((e) => { + let fp = files.db.data[e] + if (!fp) { + Accounts.files.deindex(accId, e) + return null + } + return { + ...fp, + messageids: null, + owner: null, + id: e, + } + }) + .filter((e) => e) + ) + } + ) + + fileApiRoutes.post( + "/manage", + requiresScopes("manage_files"), + async (ctx) => { + let acc = ctx.get("account") as Accounts.Account + const body = await ctx.req.json() + if (!acc) return + if ( + !body.target || + !(typeof body.target == "object") || + body.target.length < 1 + ) + return + + let modified = 0 + + body.target.forEach((e: string) => { + if (!acc.files.includes(e)) return + + let fp = files.db.data[e] + + switch (body.action) { + case "delete": + files.unlink(e, true) + modified++ + break + + case "changeFileVisibility": + if ( + !["public", "anonymous", "private"].includes( + body.value + ) + ) + return + files.db.data[e].visibility = body.value + modified++ + break + + case "setTag": + if (!body.value) delete files.db.data[e].tag + else { + if (body.value.toString().length > 30) return + files.db.data[e].tag = body.value + .toString() + .toLowerCase() + } + modified++ + break + } + }) + + return Accounts.Db.save() + .then(() => { + writeFile( + process.cwd() + "/.data/files.json", + JSON.stringify(files.db.data) + ) + }) + .then(() => ctx.text(`modified ${modified} files`)) + .catch((err) => console.error(err)) + } + ) + + return fileApiRoutes +} diff --git a/src/server/routes/api/v0/primaryApi.ts b/src/server/routes/api/v0/primaryApi.ts new file mode 100644 index 0000000..04bc00e --- /dev/null +++ b/src/server/routes/api/v0/primaryApi.ts @@ -0,0 +1,44 @@ +import { Context, Hono } from "hono" +import * as Accounts from "../../../lib/accounts.js" +import * as auth from "../../../lib/auth.js" +import RangeParser, { type Range } from "range-parser" +import ServeError from "../../../lib/errors.js" +import Files, { WebError } from "../../../lib/files.js" +import { getAccount, mirror, requiresScopes } from "../../../lib/middleware.js" +import {Readable} from "node:stream" +import type {ReadableStream as StreamWebReadable} from "node:stream/web" +import formidable from "formidable" +import { HttpBindings } from "@hono/node-server" +import { type StatusCode } from "hono/utils/http-status" +export let primaryApi = new Hono<{ + Variables: { + account: Accounts.Account + }, + Bindings: HttpBindings +}>() + +primaryApi.all("*", getAccount) + +function fileReader(apiRoot: Hono) { + return async (ctx: Context) => + apiRoot.fetch( + new Request( + (new URL( + `/api/v1/file/${ctx.req.param("fileId")}`, ctx.req.raw.url)).href, + ctx.req.raw + ), + ctx.env + ) +} + +export default function (files: Files, apiRoot: Hono) { + + primaryApi.get("/file/:fileId", fileReader(apiRoot)) + primaryApi.get("/cpt/:fileId/*", fileReader(apiRoot)) + + primaryApi.post("/upload", async (ctx) => + mirror(apiRoot, ctx, "/api/v1/file", {method: "PUT"}) + ) + + return primaryApi +} diff --git a/src/server/routes/api/v1/account/access.ts b/src/server/routes/api/v1/account/access.ts new file mode 100644 index 0000000..441d711 --- /dev/null +++ b/src/server/routes/api/v1/account/access.ts @@ -0,0 +1,127 @@ +// Modules + +import { type Context, Hono } from "hono" +import { getCookie, setCookie } from "hono/cookie" + +// Libs + +import Files from "../../../../lib/files.js" +import * as Accounts from "../../../../lib/accounts.js" +import * as auth from "../../../../lib/auth.js" +import { + assertAPI, + getAccount, + getTarget, + issuesToMessage, + login, + noAPIAccess, + requiresAccount, + requiresScopes, + scheme, +} from "../../../../lib/middleware.js" +import ServeError from "../../../../lib/errors.js" + +import Configuration from "../../../../lib/config.js" +import { AccountSchemas, AuthSchemas, FileSchemas } from "../../../../lib/schemas/index.js" +import { z } from "zod" +import { BlankInput } from "hono/types" + +type HonoEnv = { + Variables: { + account: Accounts.Account + target: Accounts.Account + targetToken: auth.AuthToken + parsedScheme: any + } +} + +const router = new Hono() + +function getTargetToken(ctx: Context) { + return auth.Db.data.find( + e => + e.account == ctx.get("target").id + && e.id == ctx.req.param("token") + ) +} + +router.use(getAccount, requiresAccount, getTarget) +router.use("/", noAPIAccess) // idk if this is redundant but just in case +router.use("/:token", async (ctx,next) => { + let tok = getTargetToken(ctx) + let actingTok = auth.resolve((await auth.tokenFor(ctx))!)! + if (!tok) + return ServeError(ctx, 404, "token not found") + if (auth.getType(actingTok) != "User" && tok != actingTok) + return ServeError(ctx, 403, "cannot manage this token") + ctx.set("targetToken", tok) + return next() +}) + +export default function (files: Files) { + + router.get("/", async (ctx) => { + return ctx.json( + auth.Db.data.filter(e => e.account == ctx.get("target").id) + ) + }) + + router.delete( + "/", + scheme( + z.array(AuthSchemas.TokenType) + .nonempty() + .default(["User"]) + .transform(e => new Set(e)), + (c) => c.req.query("type")?.split(",") + ), + async (ctx) => { + let targets = auth.Db.data.filter( + e => + e.account == ctx.get("target").id + && ctx.get("parsedScheme").has(e.type) + ) + + targets.forEach(e => auth.invalidate(e.id)) + + return ctx.text(`deleted ${targets.length} tokens`) + } + ) + + router.get("/:token", async (ctx) => { + return ctx.json(ctx.get("targetToken")) + }) + + router.delete("/:token", async (ctx) => { + auth.invalidate(ctx.get("targetToken")) + return ctx.text(`deleted token ${ctx.get("targetToken").id}`) + }) + + const CreateTokenScheme = + z.object({ + expire: z.number().positive().nullable(), + scopes: z.union([ + z.literal("all"), + z.array(AuthSchemas.Scope).nonempty().default(["user"]) + ]) + }) + + router.post( + "/", + scheme(CreateTokenScheme), + async (ctx) => { + let params = ctx.get("parsedScheme") as z.infer + let token = auth.create( + ctx.get("target").id, + params.expire, + "ApiKey", + params.scopes == "all" + ? AuthSchemas.Scope.options + : Array.from(new Set(params.scopes)) + ) + return ctx.text(await auth.makeJwt(token.id)) + } + ) + + return router +} diff --git a/src/server/routes/api/v1/account/index.ts b/src/server/routes/api/v1/account/index.ts new file mode 100644 index 0000000..be0f344 --- /dev/null +++ b/src/server/routes/api/v1/account/index.ts @@ -0,0 +1,396 @@ +// Modules + +import { type Context, Hono } from "hono" +import { getCookie, setCookie } from "hono/cookie" + +// Libs + +import Files from "../../../../lib/files.js" +import * as Accounts from "../../../../lib/accounts.js" +import * as auth from "../../../../lib/auth.js" +import { + accountMgmtRoute, + assertAPI, + getAccount, + getTarget, + issuesToMessage, + login, + noAPIAccess, + requiresAccount, + requiresScopes, + scheme, + verifyPoi, +} from "../../../../lib/middleware.js" +import ServeError from "../../../../lib/errors.js" +import { sendMail } from "../../../../lib/mail.js" +import * as CodeMgr from "../../../../lib/codes.js" + +import Configuration from "../../../../lib/config.js" +import { AccountSchemas, FileSchemas } from "../../../../lib/schemas/index.js" +import { z } from "zod" +import * as invites from "../../../../lib/invites.js" + +const router = new Hono<{ + Variables: { + account: Accounts.Account + target: Accounts.Account + parsedScheme: any + } +}>() + +type UserUpdateParameters = Partial< + Omit & { + password: string + poi?: string + } +> +type Message = [200 | 400 | 401 | 403 | 429 | 501, string] + +// there's probably a less stupid way to do this than `K in keyof Pick` +// @Jack5079 make typings better if possible + +type Validator< + T extends keyof Partial +> = + /** + * @param actor The account performing this action + * @param target The target account for this action + * @param params Changes being patched in by the user + */ + ( + actor: Accounts.Account, + target: Accounts.Account, + params: UserUpdateParameters & + { + [K in keyof Pick< + UserUpdateParameters, + T + >]-?: UserUpdateParameters[K] + }, + ctx: Context + ) => Accounts.Account[T] | Message + +type SchemedValidator< + T extends keyof Partial +> = { + validator: Validator, + schema: z.ZodTypeAny, + noAPIAccess?: boolean, + requireProofOfIdentity?: boolean +} + +const validators: { + [T in keyof Partial]: SchemedValidator +} = { + defaultFileVisibility: { + schema: FileSchemas.FileVisibility, + validator: (actor, target, params) => { + return params.defaultFileVisibility + } + }, + email: { + schema: AccountSchemas.Account.shape.email.nullable(), + noAPIAccess: true, + requireProofOfIdentity: true, + validator: (actor, target, params, ctx) => { + + if (!Configuration.mail.enabled) return [501, "email not enabled on instance"] + + if (!params.email) { + if (target.email) { + sendMail( + target.email, + `Email disconnected`, + `Hello there! Your email address (${target.email}) has been disconnected from the monofile account ${target.username}. Thank you for using monofile.` + ).catch() + } + return undefined + } + + if (actor.admin) return params.email || undefined + + // send verification email + + const tryCode = CodeMgr.code("verifyEmail", target.id, params.email) + + if (!tryCode.success) + return [429, tryCode.error] + + const { code } = tryCode + + sendMail( + params.email, + `Hey there, ${target.username} - let's connect your email`, + `Hello there! You are recieving this message because you decided to link your email, ${ + params.email.split("@")[0] + }@${ + params.email.split("@")[1] + }, to your account, ${ + target.username + }. If you would like to continue, please click here, or go to https://${ctx.req.header( + "Host" + )}/go/verify/${code.id}.` + ) + + return [200, "please check your inbox"] + }, + }, + password: { + schema: AccountSchemas.StringPassword, + noAPIAccess: true, + requireProofOfIdentity: true, + validator: (actor, target, params) => { + if (target.email) { + sendMail( + target.email, + `Your login details have been updated`, + `Hello there! Your password on your account, ${target.username}, has been updated` + + `${actor != target ? ` by ${actor.username}` : ""}. ` + + `Please update your saved login details accordingly.` + ).catch() + } + + return Accounts.password.hash(params.password) + } + }, + username: { + schema: AccountSchemas.Username, + noAPIAccess: true, + requireProofOfIdentity: true, + validator: (actor, target, params) => { + if (Accounts.getFromUsername(params.username)) + return [400, "account with this username already exists"] + + if (target.email) { + sendMail( + target.email, + `Your login details have been updated`, + `Hello there! Your username on your account, ${target.username}, has been updated` + + `${actor != target ? ` by ${actor.username}` : ""} to ${params.username}. ` + + `Please update your saved login details accordingly.` + ).catch() + } + + return params.username + } + }, + admin: { + schema: z.boolean(), + validator: (actor, target, params) => { + if (actor.admin && !target.admin) return params.admin + else if (!actor.admin) return [400, "cannot promote yourself"] + else return [400, "cannot demote an admin"] + } + }, + suspension: { + schema: AccountSchemas.Suspension.nullable(), + validator: (actor, target, params) => { + if (!actor.admin) return [400, "only admins can modify suspensions"] + if (params.suspension) + auth.Db.data + .filter(e => e.account == target.id) + .forEach(e => auth.invalidate(e.id)) + return params.suspension || undefined + } + }, + settings: { + schema: AccountSchemas.Settings.User.partial(), + validator: (actor, target, params) => { + let base = AccountSchemas.Settings.User.default({}).parse(target.settings) + + let visit = (bse: Record, nw: Record) => { + for (let [key,value] of Object.entries(nw)) { + if (typeof value == "object") visit(bse[key], value) + else bse[key] = value + } + } + + visit(base, params.settings) + + return AccountSchemas.Settings.User.parse(base) // so that toLowerCase is called again... yeah that's it + } + }, +} + +router.use(getAccount) +router.on( + ["GET","PATCH","DELETE"], + "/:user", + requiresAccount, getTarget +) +router.on( + ["PATCH","DELETE"], + "/:user", + accountMgmtRoute +) + +function isMessage(object: any): object is Message { + return ( + Array.isArray(object) && + object.length == 2 && + typeof object[0] == "number" && + typeof object[1] == "string" + ) +} + +type Result = [ + keyof Accounts.Account, + Accounts.Account[keyof Accounts.Account], +] | Message + +const BaseUserUpdateScheme = z.object( + Object.fromEntries(Object.entries(validators).filter(e => !e[1].requireProofOfIdentity).map( + ([name, validator]) => [name, validator.schema.optional()] + )) +) + +const UserUpdateScheme = z.union([ + BaseUserUpdateScheme.extend({ + poi: z.undefined() + }).strict(), + BaseUserUpdateScheme.extend({ + poi: z.string().uuid(), + ...Object.fromEntries(Object.entries(validators).filter(e => e[1].requireProofOfIdentity).map( + ([name, validator]) => [name, validator.schema.optional()] + )) + }).strict() +]) + +export default function (files: Files) { + router.post("/", scheme(z.object({ + username: AccountSchemas.Username, + password: AccountSchemas.StringPassword, + invite: z.string().max(6) + }).omit( + !Configuration.accounts.requiredForUpload + ? { invite: true } + : {} + )), async (ctx) => { + const body = await ctx.req.json() + if (!ctx.get("account")?.admin) { + if (body.invite && !invites.has(body.invite)) + return ServeError(ctx, 400, "invite invalid") + + if (ctx.get("account")) + return ServeError(ctx, 400, "you are already logged in") + } + + if (Accounts.getFromUsername(body.username)) { + return ServeError( + ctx, + 400, + "account with this username already exists" + ) + } + + if (body.invite) + invites.use(body.invite) + + return Accounts.create(body.username, body.password) + .then((account) => { + if (!ctx.get("account")) + login(ctx, account) + return ctx.text(account.id) + }) + .catch((e) => { + console.error(e) + return ServeError(ctx, 500, e instanceof z.ZodError ? issuesToMessage(e.issues) : "internal server error") + }) + }) + + router.patch( + "/:user", + scheme( + UserUpdateScheme + ), + assertAPI( + ctx => + Object.keys(ctx.get("parsedScheme")) + .some(e => validators[e as keyof typeof validators]?.noAPIAccess) + && ctx.get("account") == ctx.get("target") + ), + async (ctx) => { + const body = ctx.get("parsedScheme") as z.infer + const actor = ctx.get("account") + const target = ctx.get("target") + + if (body.poi && !verifyPoi(target.id, body.poi)) + return ServeError(ctx, 403, "invalid proof of identity provided") + + let messages = ( + Object.entries(body).filter( + (e) => e[0] !== "poi" + ) + ).map(([x, v]) => { + let validator = validators[x as keyof typeof validators]! + + return [ + x, + validator.validator(actor, target, body as any, ctx), + ] as Result + }).map((v) => { + if (isMessage(v)) return v + target[v[0]] = v[1] as never // lol + return [200, "OK"] as Message + }) + + await Accounts.Db.save() + + if (messages.length == 1) + return ctx.text( + ...(messages[0]!.reverse() as [Message[1], Message[0]]) + ) // im sorry + else return ctx.json(messages) + } + ) + + router.delete("/:user", async (ctx) => { + let actor = ctx.get("account") + let target = ctx.get("target") + + if (actor == target && !verifyPoi(actor.id, ctx.req.query("poi"))) + return ServeError(ctx, 403, "invalid proof of identity provided") + + auth.Db.data.filter((e) => e.account == target?.id).forEach((token) => { + auth.invalidate(token.id) + }) + + await Accounts.deleteAccount(target.id) + + if (target.email) { + await sendMail( + target.email, + "Notice of account deletion", + `Your account, ${target.username}, has been removed. Thank you for using monofile.` + ).catch() + return ctx.text("OK") + } + + return ctx.text("account deleted") + }) + + router.get("/:user", async (ctx) => { + let acc = ctx.get("target") + let sessionToken = (await auth.tokenFor(ctx))! + + return ctx.json({ + ...acc, + password: undefined, + email: + auth.getType(sessionToken) == "User" || + auth.getScopes(sessionToken)?.includes("email") + ? acc.email + : undefined, + activeSessions: auth.Db.data.filter( + (e) => + e.type == "User" && + e.account == acc.id && + (e.expire == null || e.expire > Date.now()) + ).length, + }) + }) + + return router +} diff --git a/src/server/routes/api/v1/account/prove.ts b/src/server/routes/api/v1/account/prove.ts new file mode 100644 index 0000000..69ec714 --- /dev/null +++ b/src/server/routes/api/v1/account/prove.ts @@ -0,0 +1,85 @@ +// Modules + +import { type Context, Hono } from "hono" +import { getCookie, setCookie } from "hono/cookie" + +// Libs + +import Files from "../../../../lib/files.js" +import * as Accounts from "../../../../lib/accounts.js" +import * as auth from "../../../../lib/auth.js" +import { + assertAPI, + getAccount, + getTarget, + issuesToMessage, + login, + noAPIAccess, + requiresAccount, + requiresScopes, + requiresTarget, + scheme, +} from "../../../../lib/middleware.js" +import ServeError from "../../../../lib/errors.js" + +import Configuration from "../../../../lib/config.js" +import { AccountSchemas, AuthSchemas, FileSchemas } from "../../../../lib/schemas/index.js" +import { z } from "zod" +import { BlankInput } from "hono/types" +import * as CodeMgr from "../../../../lib/codes.js" + +const router = new Hono<{ + Variables: { + account?: Accounts.Account + target: Accounts.Account + parsedScheme: any + } +}>() + +router.use(getAccount, getTarget, requiresTarget, noAPIAccess) + +const ProofCreationSchema = z.object({ + password: z.string().optional(), + /*auth: AuthSchemas.2fa.any*/ // if we add 2fa... +}) + +export default function () { + + router.get("/", async (ctx) => { + return ctx.json(["none"]) // if we add 2fa in the future, return available 2fa methods + }) + + router.post("/", requiresAccount, scheme( + ProofCreationSchema + ), async (ctx) => { + + let actor = ctx.get("account") + let target = ctx.get("target") + let body = ctx.get("parsedScheme") as z.infer + + if (true /*(!actor || !actor.2fa)*/) { + // if there is no actor, + // or if the actor doesn't have 2fa + // check their password first + + if (!Accounts.password.check(target.id, body.password||"")) + return ServeError(ctx, 401, `bad password`) + } + + // if actor does have 2fa in an else block here + + const tryCode = CodeMgr.code( + "identityProof", + target.id, + Boolean(actor), // so that you can only log in with proofs created when logged out + 5 * 60 * 1000 + ) + + if (!tryCode.success) + return ServeError(ctx, 429, tryCode.error) + + return ctx.text(tryCode.code.id) + }) + + return router +} diff --git a/src/server/routes/api/v1/definition.ts b/src/server/routes/api/v1/definition.ts new file mode 100644 index 0000000..9adc151 --- /dev/null +++ b/src/server/routes/api/v1/definition.ts @@ -0,0 +1,41 @@ +import type { APIDefinition } from "../../api.js"; + +export default { + "name": "v1", + "baseURL": "/api/v1", + "mount": [ + { + "file": "account/index", + "to": "/account" + }, + { + "file": "account/access", + "to": "/account/:user/access" + }, + { + "file": "account/prove", + "to": "/account/:user/proveIdentity" + }, + "session", + { + "file": "index", + "to": "/" + }, + { + "file": "file/index", + "to": "/file" + }, + { + "file": "file/individual", + "to": "/file" + }, + { + "file": "/server/invites", + "to": "/server/invites" + }, + { + "file": "/server/run", + "to": "/server/run" + } + ] +} satisfies APIDefinition diff --git a/src/server/routes/api/v1/file/index.ts b/src/server/routes/api/v1/file/index.ts new file mode 100644 index 0000000..c329dfa --- /dev/null +++ b/src/server/routes/api/v1/file/index.ts @@ -0,0 +1,240 @@ +import { Hono } from "hono" +import * as Accounts from "../../../../lib/accounts.js" +import * as auth from "../../../../lib/auth.js" +import RangeParser, { type Range } from "range-parser" +import ServeError from "../../../../lib/errors.js" +import Files, { WebError } from "../../../../lib/files.js" +import { getAccount, requiresAccount, requiresScopes, runtimeEvaluatedScheme, scheme } from "../../../../lib/middleware.js" +import {Readable} from "node:stream" +import type {ReadableStream as StreamWebReadable} from "node:stream/web" +import formidable from "formidable" +import { HttpBindings } from "@hono/node-server" +import pkg from "../../../../lib/package.js" +import { type StatusCode } from "hono/utils/http-status" +import { z } from "zod" +import { FileSchemas } from "../../../../lib/schemas/index.js" +import config from "../../../../lib/config.js" +import { BulkFileUpdate, BulkUnprivilegedFileUpdate } from "./schemes.js" +import { applyTagMask } from "../../../../lib/apply.js" + +const router = new Hono<{ + Variables: { + account: Accounts.Account, + parsedScheme: any + }, + Bindings: HttpBindings +}>() +router.all("*", getAccount) + +export default function(files: Files) { + + router.on( + ["PUT", "POST"], + "/", + requiresScopes("manage_files"), + (ctx) => { return new Promise((resolve,reject) => { + ctx.env.incoming.removeAllListeners("data") // remove hono's buffering + + let errEscalated = false + function escalate(err:Error) { + if (errEscalated) return + errEscalated = true + console.error(err) + + if ("httpCode" in err) + ctx.status(err.httpCode as StatusCode) + else if (err instanceof WebError) + ctx.status(err.statusCode as StatusCode) + else ctx.status(400) + resolve(ctx.body(err.message)) + } + + let acc = ctx.get("account") as Accounts.Account | undefined + + if (!ctx.req.header("Content-Type")?.startsWith("multipart/form-data")) + return resolve(ctx.body("must be multipart/form-data", 400)) + + if (!ctx.req.raw.body) + return resolve(ctx.body("body must be supplied", 400)) + + if (config.accounts.requiredForUpload && !acc) + return resolve(ctx.body("instance requires you to be authenticated to upload files", 401)) + + let file = files.createWriteStream(acc?.id) + + file + .on("error", escalate) + .on("finish", async () => { + if (!ctx.env.incoming.readableEnded) await new Promise(res => ctx.env.incoming.once("end", res)) + file.commit() + .then(id => resolve(ctx.body(id!))) + .catch(escalate) + }) + + let parser = formidable({ + maxFieldsSize: 65536, + maxFileSize: files.config.maxDiscordFileSize*files.config.maxDiscordFiles, + maxFiles: 1 + }) + + let acceptNewData = true + + parser.onPart = function(part) { + if (!part.originalFilename || !part.mimetype) { + parser._handlePart(part) + return + } + // lol + if (part.name == "file") { + if (!acceptNewData || file.writableEnded) + return part.emit("error", new WebError(400, "cannot set file after previously setting up another upload")) + acceptNewData = false + file.setName(part.originalFilename || "") + file.setType(part.mimetype || "") + + file.on("drain", () => ctx.env.incoming.resume()) + file.on("error", (err) => part.emit("error", err)) + + part.on("data", (data: Buffer) => { + if (!file.write(data)) + ctx.env.incoming.pause() + }) + part.on("end", () => file.end()) + } + } + + parser.on("field", async (k,v) => { + if (k == "uploadId") { + if (files.db.data[v] && ctx.req.method == "POST") + return file.destroy(new WebError(409, "file already exists")) + file.setUploadId(v) + // I'M GONNA KILL MYSELF!!!! + } else if (k == "file") { + if (!acceptNewData || file.writableEnded) + return file.destroy(new WebError(400, "cannot set file after previously setting up another upload")) + acceptNewData = false + + let res = await fetch(v, { + headers: { + "user-agent": `monofile ${pkg.version} (+https://${ctx.req.header("Host")})` + } + }).catch(escalate) + + if (!res) return + + if (!file + .setName( + res.headers.get("Content-Disposition") + ?.match(/filename="(.*)"/)?.[1] + || v.split("/")[ + v.split("/").length - 1 + ] || "generic" + )) return + + if (res.headers.has("Content-Type")) + if (!file.setType(res.headers.get("Content-Type")!)) + return + + if (!res.ok) return file.destroy(new WebError(500, `got ${res.status} ${res.statusText}`)) + if (!res.body) return file.destroy(new WebError(500, `Internal Server Error`)) + if ( + res.headers.has("Content-Length") + && !Number.isNaN(parseInt(res.headers.get("Content-Length")!,10)) + && parseInt(res.headers.get("Content-Length")!,10) > files.config.maxDiscordFileSize*files.config.maxDiscordFiles + ) + return file.destroy(new WebError(413, `file reports to be too large`)) + + Readable.fromWeb(res.body as StreamWebReadable) + .pipe(file) + } + }) + + parser.parse(ctx.env.incoming) + .catch(e => console.error(e)) + + parser.on('error', (err) => { + escalate(err) + if (!file.destroyed) file.destroy(err) + }) + + })} + ) + + // THIS IS SHIT!!! + router.patch("/", requiresAccount, runtimeEvaluatedScheme( + (c) => c.get("account").admin ? BulkFileUpdate : BulkUnprivilegedFileUpdate + ), (ctx) => { + let actor = ctx.get("account") + let update = ctx.get("parsedScheme") as z.infer + let to = Array.from(new Set(update.to).values()) + let todo = update.do + + for (let k of to) { + if (!(k in files.db.data)) + return ServeError(ctx, 404, `file ${k} doesn't exist`) + if (!actor.admin && files.db.data[k].owner != actor.id) + return ServeError(ctx, 403, `you don't own file ${k}`) + } + + let applied: Record = {} + + if (typeof todo !== "string" && "tag" in todo) + for (let e of to) { + applied[e] = applyTagMask( + files.db.data[e].tag || [], + todo.tag as Exclude + ) + if (applied[e].length > 5) + return ServeError(ctx, 400, `too many tags for file ID ${e}`) + } + + + to.forEach( + todo == "delete" + ? e => files.unlink(e, true) + : e => files.apply(e, { + ...todo, + ...("tag" in todo ? { + tag: applied[e] + } : {}) + } as Omit & { tag: string[] }, true) + ) + + files.db.save() + Accounts.Db.save() + + return ctx.text("ok") + }) + + router.get("/", requiresAccount, + /*scheme( + z.object({ + page: z.string().refine(e => !Number.isNaN(parseInt(e,10))), + amount: z.string().refine(e => !Number.isNaN(parseInt(e,10))), + changedOn: z.string().refine(e => !Number.isNaN(parseInt(e,10))) + }).partial(), + c=>c.req.query() + ),*/ (ctx,next) => { + let queryStr = ctx.req.query() + let accId = queryStr.account + let actor = ctx.get("account") + + let target = accId + ? ( + accId == "me" + ? actor + : Accounts.resolve(accId) + ) + : null + + if (!actor.admin && target != actor) + return ServeError(ctx, 403, "can't control other users") + let d = Object.entries(files.db.data) + .map(([id, file]) => ({...file, messageids: undefined, id})) + .filter(e => (!target || e.owner == target.id)) + + return ctx.json(d) + }) + + return router +} diff --git a/src/server/routes/api/v1/file/individual.ts b/src/server/routes/api/v1/file/individual.ts new file mode 100644 index 0000000..96ccfc8 --- /dev/null +++ b/src/server/routes/api/v1/file/individual.ts @@ -0,0 +1,158 @@ +import { Hono } from "hono" +import * as Accounts from "../../../../lib/accounts.js" +import * as auth from "../../../../lib/auth.js" +import RangeParser, { type Range } from "range-parser" +import ServeError from "../../../../lib/errors.js" +import Files, { WebError } from "../../../../lib/files.js" +import { getAccount, mirror, requiresScopes } from "../../../../lib/middleware.js" +import {Readable} from "node:stream" +import type {ReadableStream as StreamWebReadable} from "node:stream/web" +import formidable from "formidable" +import { HttpBindings } from "@hono/node-server" +import { type StatusCode } from "hono/utils/http-status" + +const router = new Hono<{ + Variables: { + account: Accounts.Account + }, + Bindings: HttpBindings +}>() +router.use(getAccount) + +export default function(files: Files, apiRoot: Hono) { + + router.get("/:id", async (ctx) => { + const fileId = ctx.req.param("id") + + let acc = ctx.get("account") as Accounts.Account + + let file = files.db.data[fileId] + ctx.header("Accept-Ranges", "bytes") + ctx.header("Access-Control-Allow-Origin", "*") + ctx.header("Content-Security-Policy", "sandbox allow-scripts") + + if (file) { + ctx.header("Content-Disposition", `${ctx.req.query("attachment") == "1" ? "attachment" : "inline"}; filename="${encodeURI(file.filename.replaceAll("\n","\\n"))}"`) + ctx.header("ETag", file.md5) + if (file.lastModified) { + let lm = new Date(file.lastModified) + // TERRIFYING + ctx.header("Last-Modified", + `${['Sun','Mon','Tue','Wed','Thu','Fri','Sat'][lm.getUTCDay()]}, ${lm.getUTCDate()} ` + + `${['Jan','Feb','Mar','Apr','May','Jun',"Jul",'Aug','Sep','Oct','Nov','Dec'][lm.getUTCMonth()]}` + + ` ${lm.getUTCFullYear()} ${lm.getUTCHours().toString().padStart(2,"0")}` + + `:${lm.getUTCMinutes().toString().padStart(2,"0")}:${lm.getUTCSeconds().toString().padStart(2,"0")} GMT` + ) + } + + if (file.visibility == "private") { + if (acc?.id != file.owner) { + return ServeError(ctx, 403, "you do not own this file") + } + + let token = (await auth.tokenFor(ctx))! + + if ( + auth.getType(token) != "User" && + auth + .getScopes(token)! + .includes("private") + ) { + return ServeError(ctx, 403, "insufficient permissions") + } + } + + let range: Range | undefined + + ctx.header("Content-Type", file.mime) + if (file.sizeInBytes) { + ctx.header("Content-Length", file.sizeInBytes.toString()) + + if (file.chunkSize && ctx.req.header("Range")) { + let ranges = RangeParser(file.sizeInBytes, ctx.req.header("Range") || "") + + if (ranges) { + if (typeof ranges == "number") + return ServeError(ctx, ranges == -1 ? 416 : 400, ranges == -1 ? "unsatisfiable ranges" : "invalid ranges") + if (ranges.length > 1) return ServeError(ctx, 400, "multiple ranges not supported") + range = ranges[0] + + ctx.status(206) + ctx.header( + "Content-Length", + (range.end - range.start + 1).toString() + ) + ctx.header( + "Content-Range", + `bytes ${range.start}-${range.end}/${file.sizeInBytes}` + ) + } + } + } + + if (ctx.req.method == "HEAD") + return ctx.body(null) + + return files + .readFileStream(fileId, range) + .then(async (stream) => { + let rs = new ReadableStream({ + start(controller) { + stream.once("end", () => controller.close()) + stream.once("error", (err) => controller.error(err)) + }, + cancel(reason) { + stream.destroy(reason instanceof Error ? reason : new Error(reason)) + } + }) + stream.pipe(ctx.env.outgoing) + return new Response(rs, ctx.body(null)) + }) + .catch((err) => { + return ServeError(ctx, err.status, err.message) + }) + } else { + return ServeError(ctx, 404, "file not found") + } + }) + + router.on(["PUT", "POST"], "/:id", async (ctx) => { + ctx.env.incoming.push( + `--${ctx.req.header("content-type")?.match(/boundary=(\S+)/)?.[1]}\r\n` + + `Content-Disposition: form-data; name="uploadId"\r\n\r\n` + + ctx.req.param("id") + + "\r\n" + ) + + return apiRoot.fetch( + new Request( + (new URL( + `/api/v1/file`, ctx.req.raw.url)).href, + ctx.req.raw + ), + ctx.env + ) + }) + + router.delete("/:id", async (ctx) => + mirror(apiRoot, ctx, "/api/v1/file", { + method: "PATCH", + body: JSON.stringify({ + do: "delete", + to: [ctx.req.param("id")] + }) + }) + ) + + router.patch("/:id", async (ctx) => + mirror(apiRoot, ctx, "/api/v1/file", { + method: "PATCH", + body: JSON.stringify({ + do: await ctx.req.json(), + to: [ctx.req.param("id")] + }) + }) + ) + + return router +} diff --git a/src/server/routes/api/v1/file/schemes.ts b/src/server/routes/api/v1/file/schemes.ts new file mode 100644 index 0000000..75bab36 --- /dev/null +++ b/src/server/routes/api/v1/file/schemes.ts @@ -0,0 +1,29 @@ +import { z } from "zod"; +import { FileSchemas } from "../../../../lib/schemas/index.js"; + +export const FilePatch = FileSchemas.FilePointer +.pick({ filename: true, visibility: true }) +.extend({ + id: z.string(), + owner: z.string().nullable(), + tag: z.record(FileSchemas.FileTag, z.boolean()) +}) +.partial() + +export const FileUpdate = z.union([ + z.literal("delete"), + FilePatch +]) +export const UnprivilegedFileUpdate = z.union([ + z.literal("delete"), + FilePatch.omit({ id: true, owner: true }) +]) + +export const BulkFileUpdate = z.object({ + do: FileUpdate, + to: FileSchemas.FileId.array() +}) +export const BulkUnprivilegedFileUpdate = z.object({ + do: UnprivilegedFileUpdate, + to: FileSchemas.FileId.array() +}) \ No newline at end of file diff --git a/src/server/routes/api/v1/index.ts b/src/server/routes/api/v1/index.ts new file mode 100644 index 0000000..e073b67 --- /dev/null +++ b/src/server/routes/api/v1/index.ts @@ -0,0 +1,30 @@ +import { Hono } from "hono" +import * as Accounts from "../../../lib/accounts.js" +import { HttpBindings } from "@hono/node-server" +import config, { ClientConfiguration } from "../../../lib/config.js" +import type Files from "../../../lib/files.js" +import pkg from "../../../lib/package.js" + +const router = new Hono<{ + Variables: { + account: Accounts.Account + }, + Bindings: HttpBindings +}>() + +export default function(files: Files) { + + router.get("/", async (ctx) => + ctx.json({ + version: pkg.version, + files: Object.keys(files.db.data).length, + totalSize: Object.values(files.db.data).filter(e => e.sizeInBytes).reduce((acc,cur)=>acc+cur.sizeInBytes!,0), + maxDiscordFiles: config.maxDiscordFiles, + maxDiscordFileSize: config.maxDiscordFileSize, + accounts: config.accounts, + mailEnabled: config.mail.enabled + } as ClientConfiguration) + ) + + return router +} diff --git a/src/server/routes/api/v1/server/invites.ts b/src/server/routes/api/v1/server/invites.ts new file mode 100644 index 0000000..658580f --- /dev/null +++ b/src/server/routes/api/v1/server/invites.ts @@ -0,0 +1,40 @@ +import { Hono } from "hono" +import * as Accounts from "../../../../lib/accounts.js" +import * as auth from "../../../../lib/auth.js" +import { HttpBindings } from "@hono/node-server" +import config, { ClientConfiguration } from "../../../../lib/config.js" +import type Files from "../../../../lib/files.js" +import { getAccount, requiresAccount, requiresAdmin } from "../../../../lib/middleware.js" +import { Writable } from "node:stream" +import { Db, make, use } from "../../../../lib/invites.js" +import ServeError from "../../../../lib/errors.js" + +const router = new Hono<{ + Variables: { + account: Accounts.Account + }, + Bindings: HttpBindings +}>() + +router.use(getAccount, requiresAccount, requiresAdmin) + +export default function(files: Files) { + + // api is structured like this + // in case invites become more complicated + // in the future + // if and when the api does become more complex + // i'll probably add GET /server/invites/:invite etc + + router.post("/", async (ctx) => ctx.json({id: make()})) + router.get("/", async (ctx) => ctx.json(Db.data.map(e => ({id: e})))) + router.delete("/:invite", async (ctx) => { + if (use(ctx.req.param("invite"))) { + return ctx.json({id: ctx.req.param("invite")}) + } else { + return ServeError(ctx, 404, "invalid invite") + } + }) + + return router +} diff --git a/src/server/routes/api/v1/server/run.ts b/src/server/routes/api/v1/server/run.ts new file mode 100644 index 0000000..6d898c2 --- /dev/null +++ b/src/server/routes/api/v1/server/run.ts @@ -0,0 +1,68 @@ +import { Hono } from "hono" +import * as Accounts from "../../../../lib/accounts.js" +import * as auth from "../../../../lib/auth.js" +import { HttpBindings } from "@hono/node-server" +import config, { ClientConfiguration } from "../../../../lib/config.js" +import type Files from "../../../../lib/files.js" +import { getAccount, requiresAccount, requiresAdmin } from "../../../../lib/middleware.js" +import { Writable } from "node:stream" + +const router = new Hono<{ + Variables: { + account: Accounts.Account + }, + Bindings: HttpBindings +}>() + +router.use(getAccount, requiresAccount, requiresAdmin) + +class Collect extends Writable { + collected: {t: number, packet: Buffer}[] = [] + + _write(data: Buffer, _: string, cb: () => void) { + this.collected.push({t: Date.now(), packet: data}) + cb(); + } +} + +class VirtualConsole extends console.Console { + + readonly stdout: Collect + readonly stderr: Collect + + constructor() { + const stdout = new Collect(), stderr = new Collect() + super(stdout, stderr) + this.stdout = stdout, this.stderr = stderr + } + +} + +export default function(files: Files) { + + router.post("/", async (ctx) => { + let vconsole = new VirtualConsole() + let evaluated + try { + let fn = new Function( + "accounts", + "auth", + "files", + "console", + await ctx.req.text() + ) + + evaluated = await fn(Accounts, auth, files, vconsole) + } catch (err) { + vconsole.error(err) + } + + return ctx.json({ + stdout: vconsole.stdout.collected, + stderr: vconsole.stderr.collected, + evaluated + }) + }) + + return router +} diff --git a/src/server/routes/api/v1/session.ts b/src/server/routes/api/v1/session.ts new file mode 100644 index 0000000..95976d2 --- /dev/null +++ b/src/server/routes/api/v1/session.ts @@ -0,0 +1,70 @@ +// Modules + + +import { Hono } from "hono" +import { getCookie, setCookie } from "hono/cookie" + +// Libs + +import Files from "../../../lib/files.js" +import * as Accounts from "../../../lib/accounts.js" +import * as auth from "../../../lib/auth.js" +import { + getAccount, + login, + mirror, + requiresAccount, + scheme +} from "../../../lib/middleware.js" +import ServeError from "../../../lib/errors.js" +import { AccountSchemas } from "../../../lib/schemas/index.js" +import { z } from "zod" + +const router = new Hono<{ + Variables: { + account: Accounts.Account + } +}>() + +router.use(getAccount) + +export default function (files: Files, apiRoot: Hono) { + router.post("/",scheme(z.object({ + username: AccountSchemas.Username, + password: AccountSchemas.StringPassword + })), async (ctx) => { + const body = await ctx.req.json() + + if (ctx.get("account")) + return ServeError(ctx, 400, "you are already logged in") + + const account = Accounts.getFromUsername(body.username) + + if (!account || !Accounts.password.check(account.id, body.password)) { + return ServeError(ctx, 400, "username or password incorrect") + } + + if (account.suspension) { + if (account.suspension.until && Date.now() > account.suspension.until) delete account.suspension; + else return ServeError( + ctx, + 403, + `account ${account.suspension.until + ? `suspended until ${new Date(account.suspension.until).toUTCString()}` + : "suspended indefinitely" + }: ${account.suspension.reason}`) + } + + login(ctx, account.id) + return ctx.text("logged in") + }) + + router.on( + ["GET","DELETE"], + "/", + requiresAccount, + async ctx => + mirror(apiRoot, ctx, `/api/v1/account/me/access/${await auth.tokenFor(ctx)!}`, {}) + ) + return router +} diff --git a/src/server/routes/api/web/definition.ts b/src/server/routes/api/web/definition.ts new file mode 100644 index 0000000..02564eb --- /dev/null +++ b/src/server/routes/api/web/definition.ts @@ -0,0 +1,7 @@ +import { APIDefinition } from "../../api.js"; + +export default { + "name": "web", + "baseURL": "/", + "mount": [{ "file": "preview", "to": "/download" }, "go"] +} satisfies APIDefinition \ No newline at end of file diff --git a/src/server/routes/api/web/go.ts b/src/server/routes/api/web/go.ts new file mode 100644 index 0000000..782c9f7 --- /dev/null +++ b/src/server/routes/api/web/go.ts @@ -0,0 +1,40 @@ +import fs from "fs/promises" +import bytes from "bytes" +import ServeError from "../../../lib/errors.js" +import * as Accounts from "../../../lib/accounts.js" +import type Files from "../../../lib/files.js" +import * as CodeMgr from "../../../lib/codes.js" +import { Hono } from "hono" +import { getAccount, login } from "../../../lib/middleware.js" +export let router = new Hono<{ + Variables: { + account: Accounts.Account + } +}>() + +export default function (files: Files) { + router.get("/verify/:code", getAccount, async (ctx) => { + let currentAccount = ctx.get("account") + let code = CodeMgr.codes.verifyEmail.byId.get(ctx.req.param("code")) + + if (code) { + if (currentAccount != undefined && !code.check(currentAccount.id)) { + return ServeError(ctx, 403, "you are logged in on a different account") + } + + if (!currentAccount) { + login(ctx, code.for) + let ac = Accounts.getFromId(code.for) + if (ac) currentAccount = ac + else return ServeError(ctx, 401, "could not locate account") + } + + currentAccount.email = code.data + await Accounts.Db.save() + + return ctx.redirect('/') + } else return ServeError(ctx, 404, "code not found") + }) + + return router +} \ No newline at end of file diff --git a/src/server/routes/api/web/preview.ts b/src/server/routes/api/web/preview.ts new file mode 100644 index 0000000..68b6d2d --- /dev/null +++ b/src/server/routes/api/web/preview.ts @@ -0,0 +1,114 @@ +import fs from "fs/promises" +import bytes from "bytes" +import ServeError from "../../../lib/errors.js" +import * as Accounts from "../../../lib/accounts.js" +import type Files from "../../../lib/files.js" +import pkg from "../../../lib/package.js" +import { Hono } from "hono" +import { getAccount } from "../../../lib/middleware.js" +export let router = new Hono<{ + Variables: { + account: Accounts.Account + } +}>() + +export default function (files: Files) { + router.get("/:fileId", getAccount, async (ctx) => { + let acc = ctx.get("account") as Accounts.Account + const fileId = ctx.req.param("fileId") + const host = ctx.req.header("Host") + const file = files.db.data[fileId] + if (file) { + if (file.visibility == "private" && acc?.id != file.owner) { + return ServeError(ctx, 403, "you do not own this file") + } + + const template = await fs + .readFile(process.cwd() + "/dist/download.html", "utf8") + .catch(() => { + throw ctx.status(500) + }) + let fileOwner = file.owner + ? Accounts.getFromId(file.owner) + : undefined + + return ctx.html( + template + .replaceAll("$FileId", fileId) + .replaceAll("$Version", pkg.version) + .replaceAll( + "$FileSize", + file.sizeInBytes + ? bytes(file.sizeInBytes) + : "[File size unknown]" + ) + .replaceAll( + "$FileName", + file.filename + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + ) + .replace( + "", + (file.mime.startsWith("image/") + ? `` + : file.mime.startsWith("video/") + ? ` + + + + ` + + // quick lazy fix as a fallback + // maybe i'll improve this later, but probably not. + ((file.sizeInBytes || 0) >= 26214400 + ? ` + + ` + : "") + : "") + + (fileOwner?.settings?.links?.largeImage && + file.visibility != "anonymous" && + file.mime.startsWith("image/") + ? `` + : "") + + `\n` + ) + .replace( + "", + file.mime.startsWith("image/") + ? `
` + : file.mime.startsWith("video/") + ? `
` + : file.mime.startsWith("audio/") + ? `
` + : "" + ) + .replaceAll( + "$Uploader", + !file.owner || file.visibility == "anonymous" + ? "Anonymous" + : `@${fileOwner?.username || "Deleted User"}` + ) + ) + } else return ServeError(ctx, 404, "file not found") + }) + + return router +} \ No newline at end of file diff --git a/src/server/routes/authRoutes.ts b/src/server/routes/authRoutes.ts deleted file mode 100644 index a8de6ff..0000000 --- a/src/server/routes/authRoutes.ts +++ /dev/null @@ -1,465 +0,0 @@ -import bodyParser from "body-parser"; -import { Router } from "express"; -import * as Accounts from "../lib/accounts"; -import * as auth from "../lib/auth"; -import { sendMail } from "../lib/mail"; -import { getAccount, noAPIAccess, requiresAccount, requiresPermissions } from "../lib/middleware" -import { accountRatelimit } from "../lib/ratelimit" - -import ServeError from "../lib/errors"; -import Files, { FileVisibility, generateFileId, id_check_regex } from "../lib/files"; - -import { writeFile } from "fs"; - -let parser = bodyParser.json({ - type: ["text/plain","application/json"] -}) - -export let authRoutes = Router(); -authRoutes.use(getAccount) - -let config = require(`${process.cwd()}/config.json`) - -let files:Files - -export function setFilesObj(newFiles:Files) { - files = newFiles -} - -authRoutes.post("/login", parser, (req,res) => { - if (typeof req.body.username != "string" || typeof req.body.password != "string") { - ServeError(res,400,"please provide a username or password") - return - } - - if (auth.validate(req.cookies.auth)) return - - /* - check if account exists - */ - - let acc = Accounts.getFromUsername(req.body.username) - - if (!acc) { - ServeError(res,401,"username or password incorrect") - return - } - - if (!Accounts.password.check(acc.id,req.body.password)) { - ServeError(res,401,"username or password incorrect") - return - } - - /* - assign token - */ - - res.cookie("auth",auth.create(acc.id,(3*24*60*60*1000))) - res.status(200) - res.end() -}) - -authRoutes.post("/create", parser, (req,res) => { - if (!config.accounts.registrationEnabled) { - ServeError(res,403,"account registration disabled") - return - } - - if (auth.validate(req.cookies.auth)) return - - if (typeof req.body.username != "string" || typeof req.body.password != "string") { - ServeError(res,400,"please provide a username or password") - return - } - - /* - check if account exists - */ - - let acc = Accounts.getFromUsername(req.body.username) - - if (acc) { - ServeError(res,400,"account with this username already exists") - return - } - - if (req.body.username.length < 3 || req.body.username.length > 20) { - ServeError(res,400,"username must be over or equal to 3 characters or under or equal to 20 characters in length") - return - } - - if ((req.body.username.match(/[A-Za-z0-9_\-\.]+/) || [])[0] != req.body.username) { - ServeError(res,400,"username contains invalid characters") - return - } - - if (req.body.password.length < 8) { - ServeError(res,400,"password must be 8 characters or longer") - return - } - - Accounts.create(req.body.username,req.body.password) - .then((newAcc) => { - /* - assign token - */ - - res.cookie("auth",auth.create(newAcc,(3*24*60*60*1000))) - res.status(200) - res.end() - }) - .catch(() => { - ServeError(res,500,"internal server error") - }) -}) - -authRoutes.post("/logout", (req,res) => { - if (!auth.validate(req.cookies.auth)) { - ServeError(res, 401, "not logged in") - return - } - - auth.invalidate(req.cookies.auth) - res.send("logged out") -}) - -authRoutes.post("/dfv", requiresAccount, requiresPermissions("manage"), parser, (req,res) => { - let acc = res.locals.acc as Accounts.Account - - if (['public','private','anonymous'].includes(req.body.defaultFileVisibility)) { - acc.defaultFileVisibility = req.body.defaultFileVisibility - Accounts.save() - res.send(`dfv has been set to ${acc.defaultFileVisibility}`) - } else { - res.status(400) - res.send("invalid dfv") - } -}) - -authRoutes.post("/customcss", requiresAccount, requiresPermissions("customize"), parser, (req,res) => { - let acc = res.locals.acc as Accounts.Account - - if (typeof req.body.fileId != "string") req.body.fileId = undefined; - - if ( - - !req.body.fileId - || (req.body.fileId.match(id_check_regex) == req.body.fileId - && req.body.fileId.length <= config.maxUploadIdLength) - - ) { - acc.customCSS = req.body.fileId || undefined - if (!req.body.fileId) delete acc.customCSS - Accounts.save() - res.send(`custom css saved`) - } else { - res.status(400) - res.send("invalid fileid") - } -}) - -authRoutes.post("/embedcolor", requiresAccount, requiresPermissions("customize"), parser, (req,res) => { - let acc = res.locals.acc as Accounts.Account - - if (typeof req.body.color != "string") req.body.color = undefined; - - if ( - - !req.body.color - || (req.body.color.toLowerCase().match(/[a-f0-9]+/) == req.body.color.toLowerCase()) - && req.body.color.length == 6 - - ) { - if (!acc.embed) acc.embed = {} - acc.embed.color = req.body.color || undefined - if (!req.body.color) delete acc.embed.color - Accounts.save() - res.send(`custom embed color saved`) - } else { - res.status(400) - res.send("invalid hex code") - } -}) - -authRoutes.post("/embedsize", requiresAccount, requiresPermissions("customize"), parser, (req,res) => { - let acc = res.locals.acc as Accounts.Account - - if (typeof req.body.largeImage != "boolean") req.body.color = false; - - if (!acc.embed) acc.embed = {} - acc.embed.largeImage = req.body.largeImage - if (!req.body.largeImage) delete acc.embed.largeImage - Accounts.save() - res.send(`custom embed image size saved`) -}) - -authRoutes.post("/delete_account", requiresAccount, noAPIAccess, parser, async (req,res) => { - let acc = res.locals.acc as Accounts.Account - - let accId = acc.id - - auth.AuthTokens.filter(e => e.account == accId).forEach((v) => { - auth.invalidate(v.token) - }) - - let cpl = () => Accounts.deleteAccount(accId).then(_ => res.send("account deleted")) - - if (req.body.deleteFiles) { - let f = acc.files.map(e=>e) // make shallow copy so that iterating over it doesnt Die - for (let v of f) { - files.unlink(v,true).catch(err => console.error(err)) - } - - writeFile(process.cwd()+"/.data/files.json",JSON.stringify(files.files), (err) => { - if (err) console.log(err) - cpl() - }) - } else cpl() -}) - -authRoutes.post("/change_username", requiresAccount, noAPIAccess, parser, (req,res) => { - let acc = res.locals.acc as Accounts.Account - - if (typeof req.body.username != "string" || req.body.username.length < 3 || req.body.username.length > 20) { - ServeError(res,400,"username must be between 3 and 20 characters in length") - return - } - - let _acc = Accounts.getFromUsername(req.body.username) - - if (_acc) { - ServeError(res,400,"account with this username already exists") - return - } - - if ((req.body.username.match(/[A-Za-z0-9_\-\.]+/) || [])[0] != req.body.username) { - ServeError(res,400,"username contains invalid characters") - return - } - - acc.username = req.body.username - Accounts.save() - - if (acc.email) { - sendMail(acc.email, `Your login details have been updated`, `Hello there! Your username has been updated to ${req.body.username}. Please update your devices accordingly. Thank you for using monofile.`).then(() => { - res.send("OK") - }).catch((err) => {}) - } - - res.send("username changed") -}) - -// shit way to do this but... - -let verificationCodes = new Map() - -authRoutes.post("/request_email_change", requiresAccount, noAPIAccess, accountRatelimit({ requests: 4, per: 60*60*1000 }), parser, (req,res) => { - let acc = res.locals.acc as Accounts.Account - - - if (typeof req.body.email != "string" || !req.body.email) { - ServeError(res,400, "supply an email") - return - } - - let vcode = verificationCodes.get(acc.id) - - // delete previous if any - let e = vcode?.expiry - if (e) clearTimeout(e) - verificationCodes.delete(acc?.id||"") - - let code = generateFileId(12).toUpperCase() - - // set - - verificationCodes.set(acc.id, { - code, - email: req.body.email, - expiry: setTimeout( () => verificationCodes.delete(acc?.id||""), 15*60*1000) - }) - - // this is a mess but it's fine - - sendMail(req.body.email, `Hey there, ${acc.username} - let's connect your email`, `Hello there! You are recieving this message because you decided to link your email, ${req.body.email.split("@")[0]}@${req.body.email.split("@")[1]}, to your account, ${acc.username}. If you would like to continue, please click here, or go to https://${req.header("Host")}/auth/confirm_email/${code}.`).then(() => { - res.send("OK") - }).catch((err) => { - let e = verificationCodes.get(acc?.id||"")?.expiry - if (e) clearTimeout(e) - verificationCodes.delete(acc?.id||"") - res.locals.undoCount(); - ServeError(res, 500, err?.toString()) - }) -}) - -authRoutes.get("/confirm_email/:code", requiresAccount, noAPIAccess, (req,res) => { - let acc = res.locals.acc as Accounts.Account - - - let vcode = verificationCodes.get(acc.id) - - if (!vcode) { ServeError(res, 400, "nothing to confirm"); return } - - if (typeof req.params.code == "string" && req.params.code.toUpperCase() == vcode.code) { - acc.email = vcode.email - Accounts.save(); - - let e = verificationCodes.get(acc?.id||"")?.expiry - if (e) clearTimeout(e) - verificationCodes.delete(acc?.id||"") - - res.redirect("/") - } else { - ServeError(res, 400, "invalid code") - } -}) - -authRoutes.post("/remove_email", requiresAccount, noAPIAccess, (req,res) => { - let acc = res.locals.acc as Accounts.Account - - if (acc.email) { - delete acc.email; - Accounts.save() - res.send("email detached") - } - else ServeError(res, 400, "email not attached") -}) - -let pwReset = new Map() -let prcIdx = new Map() - -authRoutes.post("/request_emergency_login", parser, (req,res) => { - if (auth.validate(req.cookies.auth || "")) return - - if (typeof req.body.account != "string" || !req.body.account) { - ServeError(res,400, "supply a username") - return - } - - let acc = Accounts.getFromUsername(req.body.account) - if (!acc || !acc.email) { - ServeError(res, 400, "this account either does not exist or does not have an email attached; please contact the server's admin for a reset if you would still like to access it") - return - } - - let pResetCode = pwReset.get(acc.id) - - if (pResetCode && pResetCode.requestedAt+(15*60*1000) > Date.now()) { - ServeError(res, 429, `Please wait a few moments to request another emergency login.`) - return - } - - - // delete previous if any - let e = pResetCode?.expiry - if (e) clearTimeout(e) - pwReset.delete(acc?.id||"") - prcIdx.delete(pResetCode?.code||"") - - let code = generateFileId(12).toUpperCase() - - // set - - pwReset.set(acc.id, { - code, - expiry: setTimeout( () => { pwReset.delete(acc?.id||""); prcIdx.delete(pResetCode?.code||"") }, 15*60*1000), - requestedAt: Date.now() - }) - - prcIdx.set(code, acc.id) - - // this is a mess but it's fine - - sendMail(acc.email, `Emergency login requested for ${acc.username}`, `Hello there! You are recieving this message because you forgot your password to your monofile account, ${acc.username}. To log in, please click here, or go to https://${req.header("Host")}/auth/emergency_login/${code}. If it doesn't appear that you are logged in after visiting this link, please try refreshing. Once you have successfully logged in, you may reset your password.`).then(() => { - res.send("OK") - }).catch((err) => { - let e = pwReset.get(acc?.id||"")?.expiry - if (e) clearTimeout(e) - pwReset.delete(acc?.id||"") - prcIdx.delete(code||"") - ServeError(res, 500, err?.toString()) - }) -}) - -authRoutes.get("/emergency_login/:code", (req,res) => { - if (auth.validate(req.cookies.auth || "")) { - ServeError(res, 403, "already logged in") - return - } - - let vcode = prcIdx.get(req.params.code) - - if (!vcode) { ServeError(res, 400, "invalid emergency login code"); return } - - if (typeof req.params.code == "string" && vcode) { - res.cookie("auth",auth.create(vcode,(3*24*60*60*1000))) - res.redirect("/") - - let e = pwReset.get(vcode)?.expiry - if (e) clearTimeout(e) - pwReset.delete(vcode) - prcIdx.delete(req.params.code) - } else { - ServeError(res, 400, "invalid code") - } -}) - -authRoutes.post("/change_password", requiresAccount, noAPIAccess, parser, (req,res) => { - let acc = res.locals.acc as Accounts.Account - - if (typeof req.body.password != "string" || req.body.password.length < 8) { - ServeError(res,400,"password must be 8 characters or longer") - return - } - - let accId = acc.id - - Accounts.password.set(accId,req.body.password) - - auth.AuthTokens.filter(e => e.account == accId).forEach((v) => { - auth.invalidate(v.token) - }) - - if (acc.email) { - sendMail(acc.email, `Your login details have been updated`, `Hello there! This email is to notify you of a password change that you have initiated. You have been logged out of your devices. Thank you for using monofile.`).then(() => { - res.send("OK") - }).catch((err) => {}) - } - - res.send("password changed - logged out all sessions") -}) - -authRoutes.post("/logout_sessions", requiresAccount, noAPIAccess, (req,res) => { - let acc = res.locals.acc as Accounts.Account - - let accId = acc.id - - auth.AuthTokens.filter(e => e.account == accId).forEach((v) => { - auth.invalidate(v.token) - }) - - res.send("logged out all sessions") -}) - -authRoutes.get("/me", requiresAccount, requiresPermissions("user"), (req,res) => { - let acc = res.locals.acc as Accounts.Account - - let sessionToken = auth.tokenFor(req) - let accId = acc.id - res.send({ - ...acc, - sessionCount: auth.AuthTokens.filter(e => e.type != "App" && e.account == accId && (e.expire > Date.now() || !e.expire)).length, - sessionExpires: auth.AuthTokens.find(e => e.token == sessionToken)?.expire, - password: undefined, - email: - auth.getType(sessionToken) == "User" || auth.getPermissions(sessionToken)?.includes("email") - ? acc.email - : undefined - }) -}) - -authRoutes.get("/customCSS", (req,res) => { - let acc = res.locals.acc - if (acc?.customCSS) res.redirect(`/file/${acc.customCSS}`) - else res.send("") -}) diff --git a/src/server/routes/fileApiRoutes.ts b/src/server/routes/fileApiRoutes.ts deleted file mode 100644 index f64d141..0000000 --- a/src/server/routes/fileApiRoutes.ts +++ /dev/null @@ -1,97 +0,0 @@ -import bodyParser from "body-parser"; -import { Router } from "express"; -import * as Accounts from "../lib/accounts"; -import * as auth from "../lib/auth"; -import bytes from "bytes" -import {writeFile} from "fs"; - -import ServeError from "../lib/errors"; -import Files from "../lib/files"; -import { getAccount, requiresAccount, requiresPermissions } from "../lib/middleware"; - -let parser = bodyParser.json({ - type: ["text/plain","application/json"] -}) - -export let fileApiRoutes = Router(); -let files:Files - -export function setFilesObj(newFiles:Files) { - files = newFiles -} - -let config = require(`${process.cwd()}/config.json`) - -fileApiRoutes.use(getAccount); - -fileApiRoutes.get("/list", requiresAccount, requiresPermissions("user"), (req,res) => { - - let acc = res.locals.acc as Accounts.Account - - if (!acc) return - let accId = acc.id - - res.send(acc.files.map((e) => { - let fp = files.getFilePointer(e) - if (!fp) { Accounts.files.deindex(accId, e); return null } - return { - ...fp, - messageids: null, - owner: null, - id:e - } - }).filter(e=>e)) - -}) - -fileApiRoutes.post("/manage", parser, requiresPermissions("manage"), (req,res) => { - - let acc = res.locals.acc as Accounts.Account - - if (!acc) return - if (!req.body.target || !(typeof req.body.target == "object") || req.body.target.length < 1) return - - let modified = 0 - - req.body.target.forEach((e:string) => { - if (!acc.files.includes(e)) return - - let fp = files.getFilePointer(e) - - if (fp.reserved) { - return - } - - switch( req.body.action ) { - case "delete": - files.unlink(e, true) - modified++; - break; - - case "changeFileVisibility": - if (!["public","anonymous","private"].includes(req.body.value)) return; - files.files[e].visibility = req.body.value; - modified++; - break; - - case "setTag": - if (!req.body.value) delete files.files[e].tag - else { - if (req.body.value.toString().length > 30) return - files.files[e].tag = req.body.value.toString().toLowerCase() - } - modified++; - break; - } - }) - - Accounts.save().then(() => { - writeFile(process.cwd()+"/.data/files.json",JSON.stringify(files.files), (err) => { - if (err) console.log(err) - res.contentType("text/plain") - res.send(`modified ${modified} files`) - }) - }).catch((err) => console.error(err)) - - -}) \ No newline at end of file diff --git a/src/server/routes/primaryApi.ts b/src/server/routes/primaryApi.ts deleted file mode 100644 index 33558d8..0000000 --- a/src/server/routes/primaryApi.ts +++ /dev/null @@ -1,181 +0,0 @@ -import bodyParser from "body-parser"; -import express, { Router } from "express"; -import * as Accounts from "../lib/accounts"; -import * as auth from "../lib/auth"; -import axios, { AxiosResponse } from "axios" -import { type Range } from "range-parser"; -import multer, {memoryStorage} from "multer" - -import ServeError from "../lib/errors"; -import Files from "../lib/files"; -import { getAccount, requiresPermissions } from "../lib/middleware"; - -let parser = bodyParser.json({ - type: ["text/plain","application/json"] -}) - -export let primaryApi = Router(); -let files:Files - -export function setFilesObj(newFiles:Files) { - files = newFiles -} - -const multerSetup = multer({storage:memoryStorage()}) - -let config = require(`${process.cwd()}/config.json`) - -primaryApi.use(getAccount); - -primaryApi.get(["/file/:fileId", "/cpt/:fileId/*", "/:fileId"], async (req:express.Request,res:express.Response) => { - - let acc = res.locals.acc as Accounts.Account - - let file = files.getFilePointer(req.params.fileId) - res.setHeader("Access-Control-Allow-Origin", "*") - res.setHeader("Content-Security-Policy","sandbox allow-scripts") - if (req.query.attachment == "1") res.setHeader("Content-Disposition", "attachment") - - if (file) { - - if (file.visibility == "private" && acc?.id != file.owner) { - ServeError(res,403,"you do not own this file") - return - } - - let range: Range | undefined - - res.setHeader("Content-Type",file.mime) - if (file.sizeInBytes) { - res.setHeader("Content-Length",file.sizeInBytes) - - if (file.chunkSize) { - let rng = req.range(file.sizeInBytes) - if (rng) { - - // error handling - if (typeof rng == "number") { - res.status(rng == -1 ? 416 : 400).send() - return - } - if (rng.type != "bytes") { - res.status(400).send(); - return - } - - // set ranges var - let rngs = Array.from(rng) - if (rngs.length != 1) { res.status(400).send(); return } - range = rngs[0] - - } - } - } - - // supports ranges - - - files.readFileStream(req.params.fileId, range).then(async stream => { - - if (range) { - res.status(206) - res.header("Content-Length", (range.end-range.start + 1).toString()) - res.header("Content-Range", `bytes ${range.start}-${range.end}/${file.sizeInBytes}`) - } - stream.pipe(res) - - }).catch((err) => { - ServeError(res,err.status,err.message) - }) - - } else { - ServeError(res, 404, "file not found") - } - -}) - -primaryApi.head(["/file/:fileId", "/cpt/:fileId/*", "/:fileId"], (req: express.Request, res:express.Response) => { - let file = files.getFilePointer(req.params.fileId) - res.setHeader("Access-Control-Allow-Origin", "*") - res.setHeader("Content-Security-Policy","sandbox allow-scripts") - if (req.query.attachment == "1") res.setHeader("Content-Disposition", "attachment") - if (!file) { - res.status(404) - res.send() - } else { - res.setHeader("Content-Type",file.mime) - if (file.sizeInBytes) { - res.setHeader("Content-Length",file.sizeInBytes) - } - if (file.chunkSize) { - res.setHeader("Accept-Ranges", "bytes") - } - } -}) - -// upload handlers - -primaryApi.post("/upload", requiresPermissions("upload"), multerSetup.single('file'), async (req,res) => { - - let acc = res.locals.acc as Accounts.Account - - if (req.file) { - try { - let prm = req.header("monofile-params") - let params:{[key:string]:any} = {} - if (prm) { - params = JSON.parse(prm) - } - - files.uploadFile({ - owner: acc?.id, - - uploadId:params.uploadId, - name:req.file.originalname, - mime:req.file.mimetype - },req.file.buffer) - .then((uID) => res.send(uID)) - .catch((stat) => { - res.status(stat.status); - res.send(`[err] ${stat.message}`) - }) - } catch { - res.status(400) - res.send("[err] bad request") - } - } else { - res.status(400) - res.send("[err] bad request") - } -}) - -primaryApi.post("/clone", requiresPermissions("upload"), bodyParser.json({type: ["text/plain","application/json"]}) ,(req,res) => { - - let acc = res.locals.acc as Accounts.Account - - try { - axios.get(req.body.url,{responseType:"arraybuffer"}).then((data:AxiosResponse) => { - - files.uploadFile({ - owner: acc?.id, - - name:req.body.url.split("/")[req.body.url.split("/").length-1] || "generic", - mime:data.headers["content-type"], - uploadId:req.body.uploadId - },Buffer.from(data.data)) - .then((uID) => res.send(uID)) - .catch((stat) => { - res.status(stat.status); - res.send(`[err] ${stat.message}`) - }) - - }).catch((err) => { - console.log(err) - res.status(400) - res.send(`[err] failed to fetch data`) - }) - } catch { - res.status(500) - res.send("[err] an error occured") - } -}) \ No newline at end of file diff --git a/src/server/tools/cli.ts b/src/server/tools/cli.ts new file mode 100644 index 0000000..d3a4baf --- /dev/null +++ b/src/server/tools/cli.ts @@ -0,0 +1,112 @@ +import fs from "fs" +import { stat } from "fs/promises" +import Files from "../lib/files.js" +import { program } from "commander" +import { basename } from "path" +import { Writable } from "node:stream" +import config from "../lib/config.js" +import pkg from "../lib/package.js" +import { fileURLToPath } from "url" +import { dirname } from "path" + +// init data + +const __dirname = dirname(fileURLToPath(import.meta.url)) +if (!fs.existsSync(__dirname + "/../../../.data/")) + fs.mkdirSync(__dirname + "/../../../.data/") + +// discord +let files = new Files(config) + +program + .name("monocli") + .description("Quickly run monofile to execute a query or so") + .version(pkg.version) + +program + .command("list") + .alias("ls") + .description("List files in the database") + .action(() => { + Object.keys(files.db.data).forEach((e) => console.log(e)) + }) + +program + .command("download") + .alias("dl") + .description("Download a file from the database") + .argument("", "ID of the file you'd like to download") + .option("-o, --output ", "Folder or filename to output to") + .action(async (id, options) => { + await new Promise((resolve) => setTimeout(() => resolve(), 1000)) + + let fp = files.db.data[id] + + if (!fp) throw `file ${id} not found` + + let out = (options.output as string) || `./` + + if (fs.existsSync(out) && (await stat(out)).isDirectory()) + out = `${out.replace(/\/+$/, "")}/${fp.filename}` + + let filestream = await files.readFileStream(id) + + let prog = 0 + filestream.on("data", (dt) => { + prog += dt.byteLength + console.log( + `Downloading ${fp.filename}: ${Math.floor((prog / (fp.sizeInBytes ?? 0)) * 10000) / 100}% (${Math.floor(prog / (1024 * 1024))}MiB/${Math.floor((fp.sizeInBytes ?? 0) / (1024 * 1024))}MiB)` + ) + }) + + filestream.pipe(fs.createWriteStream(out)) + }) + +program + .command("upload") + .alias("up") + .description("Upload a file to the instance") + .argument("", "Path to the file you'd like to upload") + .option("-id, --fileid ", "Custom file ID to use") + .action(async (file, options) => { + await new Promise((resolve) => setTimeout(() => resolve(), 1000)) + + if (!(fs.existsSync(file) && (await stat(file)).isFile())) + throw `${file} is not a file` + + let writable = files.createWriteStream() + + writable.setName(basename(file))?.setType("application/octet-stream") + + if (options.id) writable.setUploadId(options.id) + + if (!(writable instanceof Writable)) + throw JSON.stringify(writable, null, 3) + + console.log(`started: ${file}`) + + writable.on("drain", () => { + console.log("Drained") + }) + + writable.on("finish", async () => { + console.log("Finished!") + console.log(`ID: ${await writable.commit()}`) + }) + + writable.on("pipe", () => { + console.log("Piped") + }) + + writable.on("error", (e) => { + console.error(e) + }) + + writable.on("close", () => { + console.log("Closed.") + }) + + ;(await fs.createReadStream(file)).pipe(writable) + }) + +program.parse() diff --git a/src/server/tsconfig.json b/src/server/tsconfig.json new file mode 100644 index 0000000..a5a0c6a --- /dev/null +++ b/src/server/tsconfig.json @@ -0,0 +1,107 @@ +{ + "include":["**/*"], + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2021", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "nodenext", /* Specify what module code is generated. */ + //"rootDir": "./src/", /* Specify the root folder within your source files. */ + "moduleResolution": "nodenext", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + "resolveJsonModule": true, /* Enable importing .json files. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "../../out/server", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "references": [ + { "path": "../../tsconfig.json" } + ] +} diff --git a/src/style/app/pulldown/accounts.scss b/src/style/app/pulldown/accounts.scss index b6fe02b..2961dbf 100644 --- a/src/style/app/pulldown/accounts.scss +++ b/src/style/app/pulldown/accounts.scss @@ -1,4 +1,4 @@ -.pulldown_display[name=accounts] { +.pulldown_display[data-name=accounts] { .notLoggedIn { .container_div { position:absolute; @@ -184,4 +184,42 @@ } } } +} + +@keyframes bounce { + 0% { + top: 0.25em; + }/* + 25% { + top: 0.25em; + } + 75% { + top: -0.25em; + }*/ + 100% { + top: -0.25em; + } +} + +.loader { + i { + font-style: normal; + position: relative; + + animation-name: bounce; + animation-duration: 500ms; + animation-iteration-count: infinite; + animation-direction: alternate; + top:0.25em; + + &:nth-of-type(1) { + animation-delay: 0ms; + } + &:nth-of-type(2) { + animation-delay: 125ms; + } + &:nth-of-type(3) { + animation-delay: 250ms; + } + } } \ No newline at end of file diff --git a/src/style/app/pulldown/files.scss b/src/style/app/pulldown/files.scss index 3037156..8c68557 100644 --- a/src/style/app/pulldown/files.scss +++ b/src/style/app/pulldown/files.scss @@ -1,4 +1,4 @@ -.pulldown_display[name=files] { +.pulldown_display[data-name=files] { .notLoggedIn { position:absolute; top:50%; diff --git a/src/style/app/pulldown/help.scss b/src/style/app/pulldown/help.scss index d9effa2..9d59cca 100644 --- a/src/style/app/pulldown/help.scss +++ b/src/style/app/pulldown/help.scss @@ -1,4 +1,4 @@ -.pulldown_display[name=help] { +.pulldown_display[data-name=help] { overflow-y:auto; diff --git a/src/style/app/uploader/add_new_files.scss b/src/style/app/uploader/add_new_files.scss index 598bcbe..5e9db3a 100644 --- a/src/style/app/uploader/add_new_files.scss +++ b/src/style/app/uploader/add_new_files.scss @@ -13,7 +13,7 @@ span { position:relative; - &._add_files_txt { + &.add_files_txt { font-size:16px; top:-4px; left:10px; @@ -29,7 +29,7 @@ @media screen and (max-width:500px) { font-size: 40px; - span._add_files_txt { + span.add_files_txt { font-size:20px; top:-6px; left:10px; @@ -45,7 +45,7 @@ flex-direction:row; column-gap:10px; - button, input[type=text] { + button, input[type=text], input[type=submit] { background-color:#333333; color:#DDDDDD; border:none; @@ -63,7 +63,7 @@ } } - button { + button, input[type=submit] { cursor:pointer; &:hover { diff --git a/src/style/app/uploads.scss b/src/style/app/uploads.scss index 9e8827a..ee53f35 100644 --- a/src/style/app/uploads.scss +++ b/src/style/app/uploads.scss @@ -50,7 +50,7 @@ overflow:auto; } - button { + button, input[type=submit] { cursor:pointer; background-color:#393939; color:#DDDDDD; diff --git a/src/svelte/App.svelte b/src/svelte/App.svelte index 0f63334..c864eb4 100644 --- a/src/svelte/App.svelte +++ b/src/svelte/App.svelte @@ -1,19 +1,13 @@ - -
@@ -23,7 +20,7 @@ - +
- {#if ($serverStats.accounts || {}).requiredForUpload ? !!$account.username : true} + {#if $serverStats?.accounts?.requiredForUpload ? !!$account?.username : true} Hosting {$serverStats.files || "•••"}{$serverStats?.files ?? "•••"} files — Maximum filesize is - {(($serverStats.maxDiscordFileSize || 0) * - ($serverStats.maxDiscordFiles || 0)) / - 1048576 || "•••"}MiB + + { + $serverStats?.maxDiscordFiles + ? bytes($serverStats.maxDiscordFileSize * $serverStats.maxDiscordFiles) + : "•••" + }

diff --git a/src/svelte/elem/prompts/OptionPicker.svelte b/src/svelte/elem/prompts/OptionPicker.svelte index 68cef38..bf45376 100644 --- a/src/svelte/elem/prompts/OptionPicker.svelte +++ b/src/svelte/elem/prompts/OptionPicker.svelte @@ -1,28 +1,30 @@ - @@ -46,9 +48,9 @@

{#each activeModal.modal as option (option.id)} - {#if option.inputSettings} + {#if "inputSettings" in option}
- {option.id} + {option.id.toString()} @@ -60,8 +62,8 @@ {/if}
{:else} - {/if} diff --git a/src/svelte/elem/prompts/account.js b/src/svelte/elem/prompts/account.ts similarity index 79% rename from src/svelte/elem/prompts/account.js rename to src/svelte/elem/prompts/account.ts index 27e5f31..dadccae 100644 --- a/src/svelte/elem/prompts/account.js +++ b/src/svelte/elem/prompts/account.ts @@ -1,7 +1,8 @@ -import { fetchAccountData, account, refreshNeeded } from "../stores.mjs" +import { fetchAccountData, account, refreshNeeded } from "../stores" import { get } from "svelte/store"; +import type OptionPicker from "./OptionPicker.svelte"; -export function deleteAccount(optPicker) { +export function deleteAccount(optPicker: OptionPicker) { optPicker.picker("What should we do with your files?",[ { name: "Delete my files", @@ -56,7 +57,7 @@ export function deleteAccount(optPicker) { }) } -export function userChange(optPicker) { +export function userChange(optPicker: OptionPicker) { optPicker.picker("Change username",[ { name: "New username", @@ -86,7 +87,7 @@ export function userChange(optPicker) { }) } -export function forgotPassword(optPicker) { +export function forgotPassword(optPicker: OptionPicker) { optPicker.picker("Forgot your password?",[ { name: "Username", @@ -115,7 +116,7 @@ export function forgotPassword(optPicker) { }) } -export function emailPotentialRemove(optPicker) { +export function emailPotentialRemove(optPicker: OptionPicker) { optPicker.picker("What would you like to do?",[ { name: "Set a new email", @@ -148,7 +149,7 @@ export function emailPotentialRemove(optPicker) { }) } -export function emailChange(optPicker) { +export function emailChange(optPicker: OptionPicker) { optPicker.picker("Change email",[ { name: "New email", @@ -177,7 +178,7 @@ export function emailChange(optPicker) { }) } -export function pwdChng(optPicker) { +export function pwdChng(optPicker: OptionPicker) { optPicker.picker("Change password",[ { name: "New password", @@ -209,7 +210,7 @@ export function pwdChng(optPicker) { }) } -export function customcss(optPicker) { +export function customcss(optPicker: OptionPicker) { optPicker.picker("Set custom CSS",[ { name: "Enter a file ID", @@ -225,23 +226,32 @@ export function customcss(optPicker) { } ]).then((exp) => { if (exp && exp.selected) { - fetch(`/auth/customcss`,{method:"POST", body:JSON.stringify({ - fileId:exp.fileid - })}).then((response) => { - + fetch(`/api/v1/account/customization/css`, { + method: "PUT", + body: JSON.stringify({ + fileId: exp.fileid, + }), + }).then((response) => { if (response.status != 200) { - optPicker.picker(`${response.status} ${response.headers.get("x-backup-status-message") || response.statusText || ""}`,[]) + optPicker.picker( + `${response.status} ${ + response.headers.get("x-backup-status-message") || + response.statusText || + "" + }`, + [] + ) } fetchAccountData() - refreshNeeded.set(true); + refreshNeeded.set(true) }) } }) } -export function embedColor(optPicker) { +export function embedColor(optPicker: OptionPicker) { optPicker.picker("Set embed color",[ { name: "FFFFFF", @@ -257,12 +267,21 @@ export function embedColor(optPicker) { } ]).then((exp) => { if (exp && exp.selected) { - fetch(`/auth/embedcolor`,{method:"POST", body:JSON.stringify({ - color:exp.color - })}).then((response) => { - + fetch(`/api/v1/account/customization/embed/color`, { + method: "POST", + body: JSON.stringify({ + color: exp.color, + }), + }).then((response) => { if (response.status != 200) { - optPicker.picker(`${response.status} ${response.headers.get("x-backup-status-message") || response.statusText || ""}`,[]) + optPicker.picker( + `${response.status} ${ + response.headers.get("x-backup-status-message") || + response.statusText || + "" + }`, + [] + ) } fetchAccountData() @@ -272,7 +291,7 @@ export function embedColor(optPicker) { } -export function embedSize(optPicker) { +export function embedSize(optPicker: OptionPicker) { optPicker.picker("Set embed image size",[ { name: "Large", @@ -288,16 +307,25 @@ export function embedSize(optPicker) { } ]).then((exp) => { if (exp && exp.selected !== null) { - fetch(`/auth/embedsize`,{method:"POST", body:JSON.stringify({ - largeImage:exp.selected - })}).then((response) => { - + fetch(`/api/v1/account/customization/embed/size`, { + method: "POST", + body: JSON.stringify({ + largeImage: exp.selected, + }), + }).then((response) => { if (response.status != 200) { - optPicker.picker(`${response.status} ${response.headers.get("x-backup-status-message") || response.statusText || ""}`,[]) + optPicker.picker( + `${response.status} ${ + response.headers.get("x-backup-status-message") || + response.statusText || + "" + }`, + [] + ) } fetchAccountData() }) } }) -} \ No newline at end of file +} diff --git a/src/svelte/elem/prompts/admin.js b/src/svelte/elem/prompts/admin.ts similarity index 94% rename from src/svelte/elem/prompts/admin.js rename to src/svelte/elem/prompts/admin.ts index 2562a4b..3b71701 100644 --- a/src/svelte/elem/prompts/admin.js +++ b/src/svelte/elem/prompts/admin.ts @@ -1,7 +1,8 @@ -import { fetchAccountData, fetchFilePointers, account } from "../stores.mjs" +import { fetchAccountData, fetchFilePointers, account } from "../stores" import { get } from "svelte/store"; +import type OptionPicker from "./OptionPicker.svelte"; -export function pwdReset(optPicker) { +export function pwdReset(optPicker: OptionPicker) { optPicker.picker("Reset password",[ { name: "Target user", @@ -39,7 +40,7 @@ export function pwdReset(optPicker) { }) } -export function chgOwner(optPicker) { +export function chgOwner(optPicker: OptionPicker) { optPicker.picker("Transfer file ownership",[ { name: "File ID", @@ -75,7 +76,7 @@ export function chgOwner(optPicker) { }) } -export function chgId(optPicker) { +export function chgId(optPicker: OptionPicker) { optPicker.picker("Change file ID",[ { name: "Target file", @@ -111,7 +112,7 @@ export function chgId(optPicker) { }) } -export function delFile(optPicker) { +export function delFile(optPicker: OptionPicker) { optPicker.picker("Delete file",[ { name: "File ID", @@ -140,7 +141,7 @@ export function delFile(optPicker) { }) } -export function elevateUser(optPicker) { +export function elevateUser(optPicker: OptionPicker) { optPicker.picker("Elevate user",[ { name: "Username", @@ -171,7 +172,7 @@ export function elevateUser(optPicker) { // im really lazy so i just stole this from account.js -export function deleteAccount(optPicker) { +export function deleteAccount(optPicker: OptionPicker) { optPicker.picker("What should we do with the target account's files?",[ { name: "Delete files", diff --git a/src/svelte/elem/prompts/uploads.js b/src/svelte/elem/prompts/uploads.ts similarity index 93% rename from src/svelte/elem/prompts/uploads.js rename to src/svelte/elem/prompts/uploads.ts index a9fae01..6fca833 100644 --- a/src/svelte/elem/prompts/uploads.js +++ b/src/svelte/elem/prompts/uploads.ts @@ -1,5 +1,7 @@ -import { fetchAccountData, fetchFilePointers, account } from "../stores.mjs" +import { fetchAccountData, fetchFilePointers, account } from "../stores" import { get } from "svelte/store"; +import type OptionPicker from "./OptionPicker.svelte" +import type { FilePointer } from "../../../server/lib/files"; export let options = { FV: [ @@ -51,7 +53,7 @@ export let options = { ] } -export function dfv(optPicker) { +export function dfv(optPicker: OptionPicker) { optPicker.picker("Default file visibility",options.FV).then((exp) => { if (exp && exp.selected) { fetch(`/auth/dfv`,{method:"POST", body:JSON.stringify({ @@ -68,21 +70,21 @@ export function dfv(optPicker) { }) } -export function update_all_files(optPicker) { +export function update_all_files(optPicker: OptionPicker) { optPicker.picker("You sure?",[ { name: "Yeah", icon: "/static/assets/icons/update.svg", - description: `This will make all of your files ${get(account).defaultFileVisibility || "public"}`, + description: `This will make all of your files ${get(account)?.defaultFileVisibility || "public"}`, id: true } ]).then((exp) => { if (exp && exp.selected) { fetch(`/files/manage`,{method:"POST", body:JSON.stringify({ - target:get(account).files, + target:get(account)?.files, action: "changeFileVisibility", - value: get(account).defaultFileVisibility + value: get(account)?.defaultFileVisibility })}).then((response) => { if (response.status != 200) { @@ -95,7 +97,7 @@ export function update_all_files(optPicker) { }) } -export function fileOptions(optPicker,file) { +export function fileOptions(optPicker: OptionPicker, file: FilePointer & {id:string}) { optPicker.picker(file.filename,[ { name: file.tag ? "Remove tag" : "Tag file", diff --git a/src/svelte/elem/pulldowns/Accounts.svelte b/src/svelte/elem/pulldowns/Accounts.svelte index f5baede..5fd9121 100644 --- a/src/svelte/elem/pulldowns/Accounts.svelte +++ b/src/svelte/elem/pulldowns/Accounts.svelte @@ -1,26 +1,26 @@ -
diff --git a/src/svelte/elem/stores.mjs b/src/svelte/elem/stores.mjs deleted file mode 100644 index d624dd7..0000000 --- a/src/svelte/elem/stores.mjs +++ /dev/null @@ -1,35 +0,0 @@ -import { writable } from "svelte/store" - -export let refreshNeeded = writable(false) -export let pulldownManager = writable(0) -export let account = writable({}) -export let serverStats = writable({}) -export let files = writable([]) - -export let fetchAccountData = function() { - fetch("/auth/me").then(async (response) => { - if (response.status == 200) { - account.set(await response.json()) - } else { - account.set({}) - } - }).catch((err) => { console.error(err) }) -} - -export let fetchFilePointers = function() { - fetch("/files/list", { cache: "no-cache" }).then(async (response) => { - if (response.status == 200) { - files.set(await response.json()) - } else { - files.set([]) - } - }).catch((err) => { console.error(err) }) -} - -export let refresh_stats = () => { - fetch("/server").then(async (data) => { - serverStats.set(await data.json()) - }).catch((err) => { console.error(err) }) -} - -fetchAccountData() \ No newline at end of file diff --git a/src/svelte/elem/stores.ts b/src/svelte/elem/stores.ts new file mode 100644 index 0000000..8fd4785 --- /dev/null +++ b/src/svelte/elem/stores.ts @@ -0,0 +1,54 @@ +import { writable } from "svelte/store" +//import type Pulldown from "./pulldowns/Pulldown.svelte" +import type { SvelteComponent } from "svelte" +import type { Account } from "../../server/lib/accounts" +import type { ClientConfiguration } from "../../server/lib/config" +import type { FilePointer } from "../../server/lib/files" + +export let refreshNeeded = writable(false) +export let pulldownManager = writable() +export let account = writable< + (Account & { sessionCount: number; sessionExpires: number }) | undefined +>() +export let serverStats = writable() +export let files = writable<(FilePointer & { id: string })[]>([]) + +export let fetchAccountData = function () { + fetch("/auth/me") + .then(async (response) => { + if (response.status == 200) { + account.set(await response.json()) + } else { + account.set(undefined) + } + }) + .catch((err) => { + console.error(err) + }) +} + +export let fetchFilePointers = function () { + fetch("/files/list", { cache: "no-cache" }) + .then(async (response) => { + if (response.status == 200) { + files.set(await response.json()) + } else { + files.set([]) + } + }) + .catch((err) => { + console.error(err) + }) +} + +export let refresh_stats = () => { + fetch("/server") + .then(async (data) => { + serverStats.set(await data.json()) + }) + .catch((err) => { + console.error(err) + }) +} + +fetchAccountData() diff --git a/src/svelte/elem/transition/_void.js b/src/svelte/elem/transition/_void.js deleted file mode 100644 index c9bfd0d..0000000 --- a/src/svelte/elem/transition/_void.js +++ /dev/null @@ -1,20 +0,0 @@ -import { circIn, circOut } from "svelte/easing" - -export function _void(node, { duration, easingFunc, op, prop, rTarg }) { - let rect = node.getBoundingClientRect() - - return { - duration: duration||300, - css: t => { - let eased = (easingFunc || circIn)(t) - - return ` - white-space: nowrap; - ${prop||"height"}: ${(eased)*(rect[rTarg||prop||"height"])}px; - padding: 0px; - opacity:${eased}; - overflow: clip; - ` - } - } -} \ No newline at end of file diff --git a/src/svelte/elem/transition/_void.ts b/src/svelte/elem/transition/_void.ts new file mode 100644 index 0000000..4178d1e --- /dev/null +++ b/src/svelte/elem/transition/_void.ts @@ -0,0 +1,23 @@ +import { circIn, circOut } from "svelte/easing" + +export function _void( + node: HTMLElement, + options?: { duration?:number, easingFunc?: (a:number)=>number, prop?:string, rTarg?: "height"|"width"} +) { + const { duration = 300, easingFunc = circIn, prop, rTarg } = options ?? {} + let rect = node.getBoundingClientRect() + + return { + duration, + css: (t: number) => { + let eased = easingFunc(t) + return ` + white-space: nowrap; + ${prop||"height"}: ${(eased)*(rect[rTarg || (prop && prop in rect) ? prop as keyof Omit : "height"])}px; + padding: 0px; + opacity:${eased}; + overflow: clip; + ` + } + } +} \ No newline at end of file diff --git a/src/svelte/elem/transition/padding_scaleY.js b/src/svelte/elem/transition/padding_scaleY.js deleted file mode 100644 index c19a44b..0000000 --- a/src/svelte/elem/transition/padding_scaleY.js +++ /dev/null @@ -1,18 +0,0 @@ -import { circIn, circOut } from "svelte/easing" - -export function padding_scaleY(node, { duration, easingFunc, padY, padX, op }) { - let rect = node.getBoundingClientRect() - - return { - duration: duration||300, - css: t => { - let eased = (easingFunc || circOut)(t) - - return ` - height: ${eased*(rect.height-(padY||0))}px; - ${padX&&padY ? `padding: ${(eased)*(padY)}px ${(padX)}px;` : ""} - ${op ? `opacity: ${eased};` : ""} - ` - } - } -} \ No newline at end of file diff --git a/src/svelte/elem/transition/padding_scaleY.ts b/src/svelte/elem/transition/padding_scaleY.ts new file mode 100644 index 0000000..8c88c24 --- /dev/null +++ b/src/svelte/elem/transition/padding_scaleY.ts @@ -0,0 +1,21 @@ +import { circIn, circOut } from "svelte/easing" + +function padding_scaleY(node: HTMLElement, options?: { duration?: number, easingFunc?: (a: number) => number, padY?: number, padX?: number, op?: boolean }) { + const { duration = 300, easingFunc = circOut, padY, padX, op } = options ?? {} + let rect = node.getBoundingClientRect() + + return { + duration, + css: (t:number) => { + let eased = easingFunc(t) + + return ` + height: ${eased*(rect.height-(padY||0))}px; + ${padX&&padY ? `padding: ${(eased)*(padY)}px ${(padX)}px;` : ""} + ${op ? `opacity: ${eased};` : ""} + ` + } + } +} + +export {padding_scaleY} \ No newline at end of file diff --git a/src/svelte/elem/uploader/AttachmentZone.svelte b/src/svelte/elem/uploader/AttachmentZone.svelte index 7d70559..0a22b59 100644 --- a/src/svelte/elem/uploader/AttachmentZone.svelte +++ b/src/svelte/elem/uploader/AttachmentZone.svelte @@ -1,56 +1,35 @@ -