From 63a91931dab974d07087b8edf4a4074f8388e1fe Mon Sep 17 00:00:00 2001 From: Eric Woodward Date: Tue, 17 Mar 2026 22:49:38 -0400 Subject: [PATCH] alpha release update v0.8.0 --- .editorconfig | 19 + .gitignore | 52 + .prettierrc.json | 11 + .vscode/settings.json | 38 + .yarnrc.yml | 3 + LICENSE | 21 + README.md | 30 + dist/package.json | 81 + dist/src/client/script.js | 506 +++ dist/src/client/styles.css | 717 +++ dist/src/index.d.ts | 1 + dist/src/index.js | 2 + dist/src/index.js.map | 1 + dist/src/lib/arrayDB.d.ts | 14 + dist/src/lib/arrayDB.js | 73 + dist/src/lib/arrayDB.js.map | 1 + dist/src/lib/constants.d.ts | 20 + dist/src/lib/constants.js | 24 + dist/src/lib/constants.js.map | 1 + dist/src/lib/env.d.ts | 56 + dist/src/lib/env.js | 206 + dist/src/lib/env.js.map | 1 + dist/src/lib/getConfiguration.d.ts | 7 + dist/src/lib/getConfiguration.js | 103 + dist/src/lib/getConfiguration.js.map | 1 + dist/src/lib/refreshTokensDB.d.ts | 13 + dist/src/lib/refreshTokensDB.js | 40 + dist/src/lib/refreshTokensDB.js.map | 1 + dist/src/lib/simpleDB.d.ts | 12 + dist/src/lib/simpleDB.js | 71 + dist/src/lib/simpleDB.js.map | 1 + dist/src/lib/twtxtCache.d.ts | 11 + dist/src/lib/twtxtCache.js | 31 + dist/src/lib/twtxtCache.js.map | 1 + dist/src/lib/userDB.d.ts | 12 + dist/src/lib/userDB.js | 10 + dist/src/lib/userDB.js.map | 1 + dist/src/lib/utils.d.ts | 45 + dist/src/lib/utils.js | 67 + dist/src/lib/utils.js.map | 1 + dist/src/middlewares/authCheckJWT.d.ts | 9 + dist/src/middlewares/authCheckJWT.js | 32 + dist/src/middlewares/authCheckJWT.js.map | 1 + dist/src/middlewares/csrfProtection.d.ts | 1 + dist/src/middlewares/csrfProtection.js | 27 + dist/src/middlewares/csrfProtection.js.map | 1 + dist/src/middlewares/index.d.ts | 4 + dist/src/middlewares/index.js | 5 + dist/src/middlewares/index.js.map | 1 + dist/src/middlewares/postHandler/index.d.ts | 1 + dist/src/middlewares/postHandler/index.js | 2 + dist/src/middlewares/postHandler/index.js.map | 1 + dist/src/middlewares/postHandler/login.d.ts | 10 + dist/src/middlewares/postHandler/login.js | 67 + dist/src/middlewares/postHandler/login.js.map | 1 + dist/src/middlewares/postHandler/logout.d.ts | 10 + dist/src/middlewares/postHandler/logout.js | 20 + .../src/middlewares/postHandler/logout.js.map | 1 + .../middlewares/postHandler/memoryCache.d.ts | 12 + .../middlewares/postHandler/memoryCache.js | 25 + .../postHandler/memoryCache.js.map | 1 + .../middlewares/postHandler/postHandler.d.ts | 7 + .../middlewares/postHandler/postHandler.js | 60 + .../postHandler/postHandler.js.map | 1 + dist/src/middlewares/postHandler/refresh.d.ts | 9 + dist/src/middlewares/postHandler/refresh.js | 78 + .../middlewares/postHandler/refresh.js.map | 1 + dist/src/middlewares/postHandler/twt.d.ts | 9 + dist/src/middlewares/postHandler/twt.js | 21 + dist/src/middlewares/postHandler/twt.js.map | 1 + dist/src/middlewares/putHandler/editFile.d.ts | 9 + dist/src/middlewares/putHandler/editFile.js | 23 + .../middlewares/putHandler/editFile.js.map | 1 + dist/src/middlewares/putHandler/index.d.ts | 1 + dist/src/middlewares/putHandler/index.js | 2 + dist/src/middlewares/putHandler/index.js.map | 1 + .../middlewares/putHandler/putHandler.d.ts | 7 + dist/src/middlewares/putHandler/putHandler.js | 26 + .../middlewares/putHandler/putHandler.js.map | 1 + .../queryHandler/followingHandler.d.ts | 11 + .../queryHandler/followingHandler.js | 39 + .../queryHandler/followingHandler.js.map | 1 + dist/src/middlewares/queryHandler/index.d.ts | 1 + dist/src/middlewares/queryHandler/index.js | 2 + .../src/middlewares/queryHandler/index.js.map | 1 + .../queryHandler/metadataHandler.d.ts | 17 + .../queryHandler/metadataHandler.js | 50 + .../queryHandler/metadataHandler.js.map | 1 + .../queryHandler/queryHandler.d.ts | 11 + .../middlewares/queryHandler/queryHandler.js | 58 + .../queryHandler/queryHandler.js.map | 1 + .../middlewares/queryHandler/twtHandler.d.ts | 13 + .../middlewares/queryHandler/twtHandler.js | 68 + .../queryHandler/twtHandler.js.map | 1 + dist/src/middlewares/renderApp/index.d.ts | 1 + dist/src/middlewares/renderApp/index.js | 2 + dist/src/middlewares/renderApp/index.js.map | 1 + dist/src/middlewares/renderApp/renderApp.d.ts | 7 + dist/src/middlewares/renderApp/renderApp.js | 143 + .../middlewares/renderApp/renderApp.js.map | 1 + .../renderApp/renderUploadButton.d.ts | 8 + .../renderApp/renderUploadButton.js | 23 + .../renderApp/renderUploadButton.js.map | 1 + dist/src/middlewares/uploadHandler.d.ts | 9 + dist/src/middlewares/uploadHandler.js | 129 + dist/src/middlewares/uploadHandler.js.map | 1 + dist/src/packageInfo.d.ts | 6 + dist/src/packageInfo.js | 4 + dist/src/packageInfo.js.map | 1 + dist/src/plugin.d.ts | 3 + dist/src/plugin.js | 40 + dist/src/plugin.js.map | 1 + dist/src/types.d.ts | 46 + dist/src/types.js | 2 + dist/src/types.js.map | 1 + eslint.config.js | 42 + package.json | 81 + src/client/script.js | 506 +++ src/client/styles.css | 717 +++ src/index.ts | 1 + src/lib/arrayDB.ts | 89 + src/lib/constants.ts | 29 + src/lib/env.ts | 307 ++ src/lib/getConfiguration.ts | 161 + src/lib/refreshTokensDB.ts | 58 + src/lib/simpleDB.ts | 89 + src/lib/twtxtCache.ts | 48 + src/lib/userDB.ts | 17 + src/lib/utils.ts | 88 + src/middlewares/authCheckJWT.ts | 43 + src/middlewares/csrfProtection.ts | 25 + src/middlewares/index.ts | 4 + src/middlewares/postHandler/index.ts | 1 + src/middlewares/postHandler/login.ts | 86 + src/middlewares/postHandler/logout.ts | 31 + src/middlewares/postHandler/memoryCache.ts | 36 + src/middlewares/postHandler/postHandler.ts | 69 + src/middlewares/postHandler/refresh.ts | 117 + src/middlewares/postHandler/twt.ts | 36 + src/middlewares/putHandler/editFile.ts | 38 + src/middlewares/putHandler/index.ts | 1 + src/middlewares/putHandler/putHandler.ts | 34 + .../queryHandler/followingHandler.ts | 65 + src/middlewares/queryHandler/index.ts | 1 + .../queryHandler/metadataHandler.ts | 92 + src/middlewares/queryHandler/queryHandler.ts | 88 + src/middlewares/queryHandler/twtHandler.ts | 101 + src/middlewares/renderApp/index.ts | 1 + src/middlewares/renderApp/renderApp.ts | 148 + .../renderApp/renderUploadButton.ts | 29 + src/middlewares/uploadHandler.ts | 174 + src/packageInfo.ts | 5 + src/plugin.ts | 60 + src/types.ts | 58 + tsconfig.json | 17 + types.d.ts | 9 + yarn.lock | 3848 +++++++++++++++++ 157 files changed, 10951 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 .prettierrc.json create mode 100644 .vscode/settings.json create mode 100644 .yarnrc.yml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 dist/package.json create mode 100644 dist/src/client/script.js create mode 100644 dist/src/client/styles.css create mode 100644 dist/src/index.d.ts create mode 100644 dist/src/index.js create mode 100644 dist/src/index.js.map create mode 100644 dist/src/lib/arrayDB.d.ts create mode 100644 dist/src/lib/arrayDB.js create mode 100644 dist/src/lib/arrayDB.js.map create mode 100644 dist/src/lib/constants.d.ts create mode 100644 dist/src/lib/constants.js create mode 100644 dist/src/lib/constants.js.map create mode 100644 dist/src/lib/env.d.ts create mode 100644 dist/src/lib/env.js create mode 100644 dist/src/lib/env.js.map create mode 100644 dist/src/lib/getConfiguration.d.ts create mode 100644 dist/src/lib/getConfiguration.js create mode 100644 dist/src/lib/getConfiguration.js.map create mode 100644 dist/src/lib/refreshTokensDB.d.ts create mode 100644 dist/src/lib/refreshTokensDB.js create mode 100644 dist/src/lib/refreshTokensDB.js.map create mode 100644 dist/src/lib/simpleDB.d.ts create mode 100644 dist/src/lib/simpleDB.js create mode 100644 dist/src/lib/simpleDB.js.map create mode 100644 dist/src/lib/twtxtCache.d.ts create mode 100644 dist/src/lib/twtxtCache.js create mode 100644 dist/src/lib/twtxtCache.js.map create mode 100644 dist/src/lib/userDB.d.ts create mode 100644 dist/src/lib/userDB.js create mode 100644 dist/src/lib/userDB.js.map create mode 100644 dist/src/lib/utils.d.ts create mode 100644 dist/src/lib/utils.js create mode 100644 dist/src/lib/utils.js.map create mode 100644 dist/src/middlewares/authCheckJWT.d.ts create mode 100644 dist/src/middlewares/authCheckJWT.js create mode 100644 dist/src/middlewares/authCheckJWT.js.map create mode 100644 dist/src/middlewares/csrfProtection.d.ts create mode 100644 dist/src/middlewares/csrfProtection.js create mode 100644 dist/src/middlewares/csrfProtection.js.map create mode 100644 dist/src/middlewares/index.d.ts create mode 100644 dist/src/middlewares/index.js create mode 100644 dist/src/middlewares/index.js.map create mode 100644 dist/src/middlewares/postHandler/index.d.ts create mode 100644 dist/src/middlewares/postHandler/index.js create mode 100644 dist/src/middlewares/postHandler/index.js.map create mode 100644 dist/src/middlewares/postHandler/login.d.ts create mode 100644 dist/src/middlewares/postHandler/login.js create mode 100644 dist/src/middlewares/postHandler/login.js.map create mode 100644 dist/src/middlewares/postHandler/logout.d.ts create mode 100644 dist/src/middlewares/postHandler/logout.js create mode 100644 dist/src/middlewares/postHandler/logout.js.map create mode 100644 dist/src/middlewares/postHandler/memoryCache.d.ts create mode 100644 dist/src/middlewares/postHandler/memoryCache.js create mode 100644 dist/src/middlewares/postHandler/memoryCache.js.map create mode 100644 dist/src/middlewares/postHandler/postHandler.d.ts create mode 100644 dist/src/middlewares/postHandler/postHandler.js create mode 100644 dist/src/middlewares/postHandler/postHandler.js.map create mode 100644 dist/src/middlewares/postHandler/refresh.d.ts create mode 100644 dist/src/middlewares/postHandler/refresh.js create mode 100644 dist/src/middlewares/postHandler/refresh.js.map create mode 100644 dist/src/middlewares/postHandler/twt.d.ts create mode 100644 dist/src/middlewares/postHandler/twt.js create mode 100644 dist/src/middlewares/postHandler/twt.js.map create mode 100644 dist/src/middlewares/putHandler/editFile.d.ts create mode 100644 dist/src/middlewares/putHandler/editFile.js create mode 100644 dist/src/middlewares/putHandler/editFile.js.map create mode 100644 dist/src/middlewares/putHandler/index.d.ts create mode 100644 dist/src/middlewares/putHandler/index.js create mode 100644 dist/src/middlewares/putHandler/index.js.map create mode 100644 dist/src/middlewares/putHandler/putHandler.d.ts create mode 100644 dist/src/middlewares/putHandler/putHandler.js create mode 100644 dist/src/middlewares/putHandler/putHandler.js.map create mode 100644 dist/src/middlewares/queryHandler/followingHandler.d.ts create mode 100644 dist/src/middlewares/queryHandler/followingHandler.js create mode 100644 dist/src/middlewares/queryHandler/followingHandler.js.map create mode 100644 dist/src/middlewares/queryHandler/index.d.ts create mode 100644 dist/src/middlewares/queryHandler/index.js create mode 100644 dist/src/middlewares/queryHandler/index.js.map create mode 100644 dist/src/middlewares/queryHandler/metadataHandler.d.ts create mode 100644 dist/src/middlewares/queryHandler/metadataHandler.js create mode 100644 dist/src/middlewares/queryHandler/metadataHandler.js.map create mode 100644 dist/src/middlewares/queryHandler/queryHandler.d.ts create mode 100644 dist/src/middlewares/queryHandler/queryHandler.js create mode 100644 dist/src/middlewares/queryHandler/queryHandler.js.map create mode 100644 dist/src/middlewares/queryHandler/twtHandler.d.ts create mode 100644 dist/src/middlewares/queryHandler/twtHandler.js create mode 100644 dist/src/middlewares/queryHandler/twtHandler.js.map create mode 100644 dist/src/middlewares/renderApp/index.d.ts create mode 100644 dist/src/middlewares/renderApp/index.js create mode 100644 dist/src/middlewares/renderApp/index.js.map create mode 100644 dist/src/middlewares/renderApp/renderApp.d.ts create mode 100644 dist/src/middlewares/renderApp/renderApp.js create mode 100644 dist/src/middlewares/renderApp/renderApp.js.map create mode 100644 dist/src/middlewares/renderApp/renderUploadButton.d.ts create mode 100644 dist/src/middlewares/renderApp/renderUploadButton.js create mode 100644 dist/src/middlewares/renderApp/renderUploadButton.js.map create mode 100644 dist/src/middlewares/uploadHandler.d.ts create mode 100644 dist/src/middlewares/uploadHandler.js create mode 100644 dist/src/middlewares/uploadHandler.js.map create mode 100644 dist/src/packageInfo.d.ts create mode 100644 dist/src/packageInfo.js create mode 100644 dist/src/packageInfo.js.map create mode 100644 dist/src/plugin.d.ts create mode 100644 dist/src/plugin.js create mode 100644 dist/src/plugin.js.map create mode 100644 dist/src/types.d.ts create mode 100644 dist/src/types.js create mode 100644 dist/src/types.js.map create mode 100644 eslint.config.js create mode 100644 package.json create mode 100644 src/client/script.js create mode 100644 src/client/styles.css create mode 100644 src/index.ts create mode 100644 src/lib/arrayDB.ts create mode 100644 src/lib/constants.ts create mode 100644 src/lib/env.ts create mode 100644 src/lib/getConfiguration.ts create mode 100644 src/lib/refreshTokensDB.ts create mode 100644 src/lib/simpleDB.ts create mode 100644 src/lib/twtxtCache.ts create mode 100644 src/lib/userDB.ts create mode 100644 src/lib/utils.ts create mode 100644 src/middlewares/authCheckJWT.ts create mode 100644 src/middlewares/csrfProtection.ts create mode 100644 src/middlewares/index.ts create mode 100644 src/middlewares/postHandler/index.ts create mode 100644 src/middlewares/postHandler/login.ts create mode 100644 src/middlewares/postHandler/logout.ts create mode 100644 src/middlewares/postHandler/memoryCache.ts create mode 100644 src/middlewares/postHandler/postHandler.ts create mode 100644 src/middlewares/postHandler/refresh.ts create mode 100644 src/middlewares/postHandler/twt.ts create mode 100644 src/middlewares/putHandler/editFile.ts create mode 100644 src/middlewares/putHandler/index.ts create mode 100644 src/middlewares/putHandler/putHandler.ts create mode 100644 src/middlewares/queryHandler/followingHandler.ts create mode 100644 src/middlewares/queryHandler/index.ts create mode 100644 src/middlewares/queryHandler/metadataHandler.ts create mode 100644 src/middlewares/queryHandler/queryHandler.ts create mode 100644 src/middlewares/queryHandler/twtHandler.ts create mode 100644 src/middlewares/renderApp/index.ts create mode 100644 src/middlewares/renderApp/renderApp.ts create mode 100644 src/middlewares/renderApp/renderUploadButton.ts create mode 100644 src/middlewares/uploadHandler.ts create mode 100644 src/packageInfo.ts create mode 100644 src/plugin.ts create mode 100644 src/types.ts create mode 100644 tsconfig.json create mode 100644 types.d.ts create mode 100644 yarn.lock diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c8c9992 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,19 @@ +# EditorConfig is awesome: https://editorconfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = tab +indent_style = tab +insert_final_newline = true +trim_trailing_whitespace = true + +[*.json] +indent_size = 2 +indent_style = spaces + +[*.md] +indent_style = tab +trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..71714ee --- /dev/null +++ b/.gitignore @@ -0,0 +1,52 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn setups files +.yarn +.yarn-integrity + +# dotenv environment variables file +.env + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# twtkpr data file directory +.data + +# Other files +NOTES.md +TODO.md +*.bak \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..ec4313b --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,11 @@ +{ + "printWidth": 80, + "semi": true, + "singleQuote": true, + "trailingComma": "es5", + "bracketSpacing": true, + "arrowParens": "always", + "endOfLine": "lf", + "embeddedLanguageFormatting": "auto", + "quoteProps": "as-needed" +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..e102c23 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,38 @@ +{ + "prettier.enable": true, + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "prettier.requireConfig": true, + "eslint.validate": [ + "javascript", + "javascriptreact", + "typescript", + "typescriptreact" + ], + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.unusualLineTerminators": "off", + "cSpell.words": [ + "blakejs", + "dotenv", + "eslintcache", + "itsericwoodward", + "jspm", + "nodenext", + "noframe", + "noopener", + "noreferrer", + "pgpkey", + "pids", + "TWTKPR", + "twts", + "Twttr", + "twtxt", + "uuidv", + "wscript" + ], + "[html]": { + "editor.defaultFormatter": "vscode.html-language-features" + } +} diff --git a/.yarnrc.yml b/.yarnrc.yml new file mode 100644 index 0000000..7f75bcd --- /dev/null +++ b/.yarnrc.yml @@ -0,0 +1,3 @@ +nodeLinker: node-modules + +yarnPath: .yarn/releases/yarn-4.13.0.cjs diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e0503f5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Eric Woodward [https://www.itsericwoodward.com] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..39ed141 --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# Twtkpr + +An [ExpressJS](https://expressjs.com/) router for serving, sharing, and updating a +[`twtxt.txt` file](https://twtxt.dev/). + +> [!WARNING] +> **STILL IN ALPHA**: Although this plugin lacks documentation, examples, tests, installation +> flexibility, and polish, it's still fully-functional and actively deployed to at least one site. + +### Features + +- Uses JWT (with refresh) for security. +- Allows for adding new twts and directly editing a `twtxt.txt` file from within a browser. +- Includes a ull-featured GET API for your `twtxt.txt` file backed by an in-memory cache. +- Supports optional drag-and-drop file upload handling with automatic linking. + +## Installing + +```sh +yarn add express-twtkpr +``` + +More to come! + +--- + +## License + +Copyright (c) 2026 Eric Woodward, released under the +[MIT License](https://www.itsericwoodward.com/licenses/mit/). diff --git a/dist/package.json b/dist/package.json new file mode 100644 index 0000000..9fe1c63 --- /dev/null +++ b/dist/package.json @@ -0,0 +1,81 @@ +{ + "name": "express-twtkpr", + "version": "0.8.0", + "description": "An express library for hosting and maintaining a twtxt.txt file.", + "license": "MIT", + "author": { + "name": "Eric Woodward", + "email": "hey@itsericwoodward.com", + "url": "https://www.itsericwoodward.com" + }, + "repository": { + "type": "git", + "url": "https://git.itsericwoodward.com/eric/express-twtkp" + }, + "keywords": [], + "type": "module", + "main": "./dist/src/index.js", + "module": "./dist/src/index.js", + "types": "./dist/src/index.d.ts", + "exports": { + ".": { + "import": "./dist/src/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "start": "node --env-file=.env dist/index-app.js", + "build": "tsc && cp -r src/client dist/src", + "dev": "DEBUG='twtkpr:*' tsx watch --env-file=.env src/index-app.ts", + "get:hash": "tsx --env-file=.env src/cli.ts get-hash", + "lint": "eslint --fix src test", + "prepublishOnly": "yarn build", + "set:user": "tsx --env-file=.env src/cli.ts set-user", + "test": "vitest", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@cacheable/node-cache": "^2.0.2", + "@exodus/blakejs": "^1.1.1-exodus.0", + "base32.js": "^0.1.0", + "bcryptjs": "^3.0.3", + "cookie-parser": "^1.4.7", + "dayjs": "^1.11.20", + "debug": "^4.4.3", + "express": "^5.2.1", + "express-rate-limit": "^8.3.1", + "express-session": "^1.19.0", + "express-slow-down": "^3.1.0", + "formidable": "^3.5.4", + "jsonwebtoken": "^9.0.3", + "link": "^2.1.2", + "session-file-store": "^1.5.0", + "twtxt-lib": "^0.9.4", + "uuid": "^13.0.0", + "zod": "^4.3.6" + }, + "devDependencies": { + "@types/cookie-parser": "^1.4.10", + "@types/cors": "^2.8.19", + "@types/debug": "^4.1.12", + "@types/express": "^5.0.6", + "@types/express-session": "^1.18.2", + "@types/formidable": "^3.5.0", + "@types/jsonwebtoken": "^9.0.10", + "@types/morgan": "^1.9.10", + "@types/node": "^25.5.0", + "@types/supertest": "^7.2.0", + "eslint": "^10.0.3", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.5", + "eslint-plugin-security": "^4.0.0", + "prettier": "^3.8.1", + "supertest": "^7.2.2", + "tsx": "^4.21.0", + "typescript": "^5.9.3", + "vitest": "^4.1.0" + }, + "packageManager": "yarn@4.13.0" +} diff --git a/dist/src/client/script.js b/dist/src/client/script.js new file mode 100644 index 0000000..7981616 --- /dev/null +++ b/dist/src/client/script.js @@ -0,0 +1,506 @@ +const DEBUG_ON = true; + +// served from same path as TWTXT file +const TWTXT_FILE_URL = window.location.pathname; +const REMEMBER_LOGIN_STORAGE_KEY = 'rememberLogin'; +const ACCESS_TOKEN_COOKIE_KEY = 'accessToken'; + +const debug = (...vals) => { + if (DEBUG_ON) console.log(...vals); +}; + +export default (async () => { + /* DOM Elements */ + const twtForm = document.getElementById('twtForm'), + loginForm = document.getElementById('loginControls-form'), + fileBox = document.getElementById('fileBox'), + fileContentsSection = document.getElementById('fileContentsSection'), + toastContainer = document.getElementById('toast-container'), + twtControlsContentInput = document.getElementById( + 'twtControlsContentInput' + ), + twtSubmitButton = document.querySelector('.twtControls-submitButton'), + twtLogoutButton = document.getElementById('twtControlsLogoutButton'), + twtFileEditButton = document.getElementById('twtControlsEditButton'), + menuCheckbox = document.getElementById('hamburgerToggleCheckbox'), + twtxtEditFormText = document.getElementById('twtxtEditFormText'), + uploadInputs = document.querySelectorAll('.twtControls-uploadInput'), + rememberToggle = document.getElementById('loginControls-rememberToggle'); + + const lastModifiedDates = {}; + let isEditing = false, + cookie, + fileText, + token; + + const showToast = (message, type = 'success') => { + const toast = document.createElement('div'); + toast.classList.add('toast'); + if (type === 'error') toast.classList.add('error'); + toast.textContent = message; + + toastContainer.appendChild(toast); + setTimeout(() => { + toast.style.animation = 'fadeOut 0.5s forwards'; + setTimeout(() => toast.remove(), 500); + }, 3000); + }; + + const beginEditMode = () => { + isEditing = true; + + if (menuCheckbox) menuCheckbox.checked = false; + if (twtSubmitButton) twtSubmitButton.setAttribute('disabled', 'disabled'); + document.body.classList.add('js-editMode'); + }; + + const endEditMode = () => { + isEditing = false; + + if (twtSubmitButton) twtSubmitButton.removeAttribute('disabled'); + document.body.classList.remove('js-editMode'); + }; + + const getCookie = (name) => { + const cookies = document.cookie.split('; '); + for (const cookie of cookies) { + const [key, value] = cookie.split('='); + if (key === name) return decodeURIComponent(value); + } + return null; + }; + + const setCookie = (name, value, expireDays) => { + const isSecure = window.location.protocol === 'https'; + const expireDate = new Date(); // current date + expireDate.setTime( + expireDate.getTime() + (expireDays ?? 0) * 24 * 60 * 60 * 1000 + ); + let expires = + expireDays !== undefined ? `expires=${expireDate.toUTCString()}; ` : ''; + document.cookie = `${name}=${encodeURIComponent(value)}; ${expires}${isSecure ? 'Secure; ' : ''}SameSite=Strict; Path=/`; + }; + + const loadTwtxtFile = async (filePath = TWTXT_FILE_URL) => { + debug('loadTwtxtFile start'); + let response; + const fileURL = `${filePath.charAt(0) !== '/' ? '/' : ''}${filePath}`; + + const headers = new Headers(); + headers.set( + 'Accept', + 'application/twtxt+txt,text/twtxt,text/plain;q=0.9,*/*;q=0.8' + ); + + if (lastModifiedDates[fileURL]) { + headers['If-Modified-Since'] = lastModifiedDates[fileURL]; + } + + try { + response = await fetch(fileURL, { + method: 'GET', + headers, + mode: 'same-origin', + }); + + if (response?.body) fileText = await new Response(response.body).text(); + + if (response?.headers.get('Last-Modified')) { + lastModifiedDates[fileURL] = response?.headers.get('Last-Modified'); + } + + if (fileText && fileBox) { + fileBox.textContent = fileText; + } + } catch (err) { + showToast('Unable to load file, please try again later.', 'error'); + console.error('Error loading file', err); + } + debug('loadTwtxtFile end'); + }; + + const refreshToken = async (hideToast = false) => { + debug('refreshToken start', hideToast); + const rememberToggleVal = + localStorage.getItem(REMEMBER_LOGIN_STORAGE_KEY) === 'true'; + + const res = await fetch(`${TWTXT_FILE_URL}`, { + method: 'POST', + body: new URLSearchParams({ + rememberToggle: rememberToggleVal, + type: 'refresh', + }), + credentials: 'include', // Include cookies + }); + + if (res.ok && res?.body) { + token = await new Response(res.body).text(); + if (rememberToggleVal) { + debug('refreshToken set new accessToken cookie'); + setCookie(ACCESS_TOKEN_COOKIE_KEY, token); + } + debug('refreshToken end OK'); + return; + } + + // Handle refresh failure + if (!hideToast) + showToast('Unable to refresh token, please try again later.', 'error'); + token = undefined; + document.body.classList.remove('js-authorized'); + debug('refreshToken end error'); + throw new Error('Failed to refresh token'); + }; + + const uploadFiles = async (files, uploadRoute, secondAttempt = false) => { + if (!uploadRoute) return; + + debug('uploadFiles', token, files, uploadRoute, secondAttempt); + + const formData = new FormData(); + for (let i = 0; i < files.length; i++) { + formData.append('files', files[i]); + } + + try { + const res = await fetch(uploadRoute, { + method: 'POST', + body: formData, + headers: { + Authorization: `Bearer ${token}`, + }, + credentials: 'include', + }); + + if (res.ok) { + showToast(`File${files.length !== 1 ? 's' : ''} uploaded`); + + const filePath = await res.text(); + twtControlsContentInput.value += filePath + .split('\n') + .map((currFilePath) => + [ + ' ', + location.protocol, + '//', + location.hostname, + location.protocol !== 'https' && location.port !== 80 + ? ':' + location.port + : '', + currFilePath, + ].join('') + ) + .join(''); + + return; + } + + if (!secondAttempt) { + await refreshToken(); + return uploadFiles(files, uploadRoute, true); + } + + showToast( + `Unable to upload image${files.length !== 1 ? 's' : ''} refresh token, please try again later.`, + 'error' + ); + } catch (err) { + console.error(err); + } + }; + + /* Handlers */ + + const dragOverHandler = (ev) => { + const files = [...ev.dataTransfer.items].filter( + (item) => item.kind === 'file' + ); + + if (files.length > 0) { + ev.preventDefault(); + ev.dataTransfer.dropEffect = 'copy'; + } + }; + + const dragOverWindowHandler = (ev) => { + const files = [...ev.dataTransfer.items].filter( + (item) => item.kind === 'file' + ); + + if (files.length > 0) { + ev.preventDefault(); + + if (!twtControlsContentInput.contains(ev.target)) { + ev.dataTransfer.dropEffect = 'none'; + } + } + }; + + const dropHandler = (ev) => { + ev.preventDefault(); + + if (!uploadInputs.length) return; + + const files = [...ev.dataTransfer.items] + .map((item) => item.getAsFile()) + .filter((file) => file); + + debug('dropHandler', files); + uploadFiles(files, uploadInputs[0].getAttribute('data-route')); + }; + + const dropWindowHandler = (ev) => { + if ([...ev.dataTransfer.items].some((item) => item.kind === 'file')) { + ev.preventDefault(); + } + }; + + const editClickHandler = () => { + if (isEditing) return; + + if (twtxtEditFormText) twtxtEditFormText.value = fileText; + + beginEditMode(); + }; + + const editResetHandler = (ev) => { + ev.preventDefault(); + + if (!isEditing && !confirm('Do you want to quit editing?')) return; + + if (fileText && fileBox) { + fileBox.textContent = fileText; + } + + endEditMode(); + }; + + const editSubmitHandler = async (ev, secondAttempt = false) => { + ev?.preventDefault(); + + try { + const newFileText = twtxtEditFormText.value; + + debug('edit file submit', { newFileText }); + if (!newFileText) return; + + const res = await fetch(`${TWTXT_FILE_URL}`, { + method: 'PUT', + body: new URLSearchParams({ + fileContents: newFileText, + }), + headers: { + Authorization: `Bearer ${token}`, + }, + credentials: 'include', + }); + + if (!res.ok && !secondAttempt) { + await refreshToken(); + return editSubmitHandler(ev, true); + } + + showToast('File updated'); + + if (newFileText && fileBox) { + fileText = newFileText; + fileBox.textContent = newFileText; + } + + endEditMode(); + + await loadTwtxtFile(); + } catch (err) { + if (!secondAttempt) { + await refreshToken(); + return twtFormSubmitHandler(ev, true); + } + + showToast('Unable to update file, please try again later.', 'error'); + console.error('Error updating file', err); + } + + return false; + }; + + const loginFormSubmitHandler = async (ev) => { + ev?.preventDefault(); + let response; + + try { + const loginData = new URLSearchParams(new FormData(loginForm)); + debug('loginForm submit', { loginData }); + + response = await fetch(TWTXT_FILE_URL, { + method: 'POST', + body: loginData, + mode: 'same-origin', + credentials: 'include', + }); + + debug('loginForm submit', { response }); + + if (!response.ok) throw new Error(response.statusText); + showToast('Login complete'); + } catch (err) { + showToast('Unable to login, please try again later.', 'error'); + console.error('Error logging in', err); + + return; + } + + if (response?.body) token = await new Response(response.body).text(); + + if (token) document.body.classList.add('js-authorized'); + + if (token && rememberToggle.checked) + setCookie(ACCESS_TOKEN_COOKIE_KEY, token, 7); + + debug('loginForm submit', { cookie, token, response }); + + return false; + }; + + const logoutHandler = async () => { + try { + const res = await fetch(`${TWTXT_FILE_URL}`, { + method: 'POST', + body: new URLSearchParams({ + type: 'logout', + }), + credentials: 'include', + }); + + if (!res.ok) throw new Error(); + + document.cookie = `${ACCESS_TOKEN_COOKIE_KEY}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; + + if (menuCheckbox) menuCheckbox.checked = false; + + window.location.reload(); + } catch { + showToast('Unable to logout, please try again later.', 'error'); + } + }; + + const rememberToggleHandler = (ev) => { + if (ev.target.checked) { + localStorage.setItem(REMEMBER_LOGIN_STORAGE_KEY, 'true'); + return; + } + + localStorage.removeItem(REMEMBER_LOGIN_STORAGE_KEY); + }; + + const twtContentKeyupHandler = (ev) => { + const hasContent = + (twtControlsContentInput.value?.trim() ?? '').length !== 0; + + if (twtControlsContentInput) { + requestAnimationFrame(() => { + // Sync with browser repaint + twtControlsContentInput.style.height = 'auto'; + twtControlsContentInput.style.height = `${Math.max(twtControlsContentInput.scrollHeight, 80)}px`; + }); + } + + if (isEditing || !hasContent) + twtSubmitButton.setAttribute('disabled', 'disabled'); + else twtSubmitButton.removeAttribute('disabled'); + + if (hasContent && ev?.key === 'Enter' && ev?.ctrlKey) + twtFormSubmitHandler(); + }; + + const twtFormSubmitHandler = async (ev, secondAttempt = false) => { + ev?.preventDefault(); + + try { + const twtData = new FormData(twtForm); + const twtContent = twtData.get('content').trim(); + + debug('twtForm submit data', { twtData }); + if (!twtContent) return; + + twtData.set('content', twtContent.replaceAll('\n', '\u2028')); + const twtBody = new URLSearchParams(twtData); + debug('twtForm submit body', { twtBody }); + + const res = await fetch(TWTXT_FILE_URL, { + method: 'POST', + body: twtBody, + headers: { + Authorization: `Bearer ${token}`, + }, + credentials: 'include', + }); + + if (!res.ok && !secondAttempt) { + debug('twtForm submit - not OK, trying refresh'); + await refreshToken(); + return twtFormSubmitHandler(ev, true); + } + + showToast('Twt sent'); + + twtForm.reset(); + await loadTwtxtFile(); + + // scroll to bottom of file to show update + fileContentsSection.scrollTop = fileBox.scrollHeight; + } catch (err) { + debug('twtForm submit - error, trying refresh', err, !secondAttempt); + if (!secondAttempt) { + await refreshToken(); + return twtFormSubmitHandler(ev, true); + } + + showToast('Unable to twt, please try again later.', 'error'); + console.error('Error POSTing twt', err); + } + return false; + }; + + const uploadChangeHandler = (ev) => { + uploadFiles(ev.target.files, ev.target.getAttribute('data-route')); + }; + + /* Attach Handlers to Listeners */ + + Array.from(uploadInputs).forEach((uploadInput) => { + uploadInput.addEventListener('change', uploadChangeHandler); + }); + + loginForm.addEventListener('submit', loginFormSubmitHandler); + + twtForm.addEventListener('submit', twtFormSubmitHandler); + + twtForm.addEventListener('keyup', twtContentKeyupHandler); + + twtControlsContentInput.addEventListener('drop', dropHandler); + + twtControlsContentInput.addEventListener('dragover', dragOverHandler); + + twtLogoutButton.addEventListener('click', logoutHandler); + + twtFileEditButton.addEventListener('click', editClickHandler); + + twtxtEditForm.addEventListener('reset', editResetHandler); + + twtxtEditForm.addEventListener('submit', editSubmitHandler); + + window.addEventListener('dragover', dragOverWindowHandler); + + window.addEventListener('drop', dropWindowHandler); + + rememberToggle.addEventListener('change', rememberToggleHandler); + + /* Start App*/ + + loadTwtxtFile().catch(() => {}); + + token = getCookie(ACCESS_TOKEN_COOKIE_KEY); + if (token) document.body.classList.add('js-authorized'); + + debug('client loaded'); +})(); diff --git a/dist/src/client/styles.css b/dist/src/client/styles.css new file mode 100644 index 0000000..1a976e8 --- /dev/null +++ b/dist/src/client/styles.css @@ -0,0 +1,717 @@ +/** + * Taken from Normalize.css v12.1.1 / https://csstools.github.io/normalize.css/ + */ +:where(html) { + line-height: 1.15; + -webkit-text-size-adjust: 100%; + text-size-adjust: 100%; +} +:where(b, strong) { + font-weight: bolder; +} +:where(code, kbd, pre, samp) { + font-family: monospace, monospace; + font-size: 1em; +} +:where(button, input, select) { + margin: 0; +} +:where(button) { + text-transform: none; +} +:where( + button, + input:is([type='button' i], [type='reset' i], [type='submit' i]) +) { + -webkit-appearance: button; + appearance: button; +} +:where(select) { + text-transform: none; +} +:where(textarea) { + margin: 0; +} +:where(input[type='search' i]) { + -webkit-appearance: textfield; + appearance: textfield; + outline-offset: -2px; +} +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} +::-webkit-input-placeholder { + color: inherit; + opacity: 0.54; +} +::-webkit-search-decoration { + -webkit-appearance: none; +} +::-webkit-file-upload-button { + -webkit-appearance: button; + font: inherit; +} +:where( + button, + input:is( + [type='button' i], + [type='color' i], + [type='reset' i], + [type='submit' i] + ) +)::-moz-focus-inner { + border-style: none; + padding: 0; +} +:where( + button, + input:is( + [type='button' i], + [type='color' i], + [type='reset' i], + [type='submit' i] + ) +)::-moz-focusring { + outline: 1px dotted ButtonText; +} + +/** + * Local styles + */ +:root { + /* #0a0a14 / Vulcan*/ + --bg: rgb(10, 10, 20); + --bg-al: rgb(10, 10, 20, 0.6); + + /* #1b1b27 / Steel Gray */ + --bg-hl: rgb(27, 27, 39); + + /* #6e6e81 / Storm Gray */ + --fg: rgb(110, 110, 129); + + --fg-hl: #ccc; + + /* #9f9fc1 / Logan */ + --link: rgb(159, 159, 193); +} + +* { + box-sizing: border-box; +} + +body { + background-color: var(--bg); + color: var(--fg); +} + +a { + border-radius: 0.5rem; + color: var(--link); + padding: 0 0.25rem; + transition: all 0.5s; +} + +a:hover { + color: var(--fg-hl); + background-color: var(--bg-hl); +} + +h1, +h2, +h3, +h4, +h5, +h6 { + color: var(--fg-hl); +} + +input, +textarea { + background-color: var(--bg-hl); + border-color: var(--fg); + color: var(--fg); +} + +button, +input[type='reset'], +input[type='submit'] { + background-color: var(--bg-hl); + border-color: var(--link); + color: var(--link); +} + +button:disabled, +input:disabled { + opacity: 0.2; +} + +.appInfo { + font-size: smaller; + font-style: italic; + margin: 0.5rem auto; + text-align: center; +} + +.button { + background-color: var(--bg-hl); + border: 1px solid var(--link); + border-radius: 0.5rem; + color: var(--link); + padding: 0.5rem; +} + +.fileContentsSection { + margin-bottom: 7rem; + max-width: 100vw; + overflow: auto; +} + +.fileContentsSection-fileBox { + margin: 0; + max-height: 1000rem; + overflow-y: hidden; + transition: all 0.5s; +} + +.hamburgerToggle { + grid-area: menuButton; + max-width: 3rem; +} + +.hamburgerToggle-icon, +.hamburgerToggle-icon:after, +.hamburgerToggle-icon:before { + background-color: var(--fg); + height: 0.25rem; + position: absolute; + transition-duration: 0.5s; + width: 2rem; +} + +.hamburgerToggle-icon { + top: 12px; +} + +.hamburgerToggle-icon:before { + content: ''; + top: -12px; +} + +.hamburgerToggle-icon:after { + content: ''; + top: 12px; +} + +.hamburgerToggle-label { + cursor: pointer; + display: block; + height: 1.75rem; + left: 0; + position: relative; + top: 0; + transition-duration: 0.5s; + width: 2rem; +} + +#hamburgerToggleCheckbox { + opacity: 0; + cursor: pointer; + position: absolute; +} + +#hamburgerToggleCheckbox:checked + + .hamburgerToggle-label + .hamburgerToggle-icon { + transition-duration: 0.5s; + background: transparent; +} + +#hamburgerToggleCheckbox:checked + + .hamburgerToggle-label + .hamburgerToggle-icon:before { + transform: rotateZ(45deg) scaleX(1.25) translate(8px, 8px); +} + +#hamburgerToggleCheckbox:checked + + .hamburgerToggle-label + .hamburgerToggle-icon:after { + transform: rotateZ(-45deg) scaleX(1.25) translate(8px, -8px); +} + +#hamburgerToggleCheckbox:checked ~ .popupMenu { + right: 0.5rem; +} + +.loginControls { + max-height: 100rem; + overflow: hidden; +} + +.loginControls-fields { + display: flex; + flex-direction: column; + gap: 0.5rem; + justify-content: end; +} + +.loginControls-fields-row { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 0.5rem; + justify-content: end; +} + +.loginControls-input { + max-width: 8rem; +} + +.loginControls input:focus { + border: 1px solid #eee; +} + +.loginControls-label { + margin-right: 1rem; +} + +.loginControls-row { + align-items: center; + display: flex; + flex-direction: row; + justify-content: end; +} + +.loginControls-toggle { + align-items: center; + border-radius: 8rm; + display: flex; + justify-content: end; + margin-right: 1rem; +} + +.loginControls-toggle-checkbox { + clip: rect(0 0 0 0); + clip-path: inset(50%); + height: 1px; + overflow: hidden; + position: absolute; + white-space: nowrap; + width: 1px; +} + +.loginControls-toggle-checkbox:not([disabled]):active + + .loginControls-toggle-track, +.loginControls-toggle-checkbox:not([disabled]):focus + + .loginControls-toggle-track { + /* border: 1px solid transparent; */ + box-shadow: 0px 0px 0px 2px #333; +} +.loginControls-toggle-checkbox:disabled + .loginControls-toggle-track { + cursor: not-allowed; + opacity: 0.7; +} + +.loginControls-toggle-track { + background: var(--bg); + border: 1px solid var(--fg); + border-radius: 100px; + cursor: pointer; + display: flex; + height: 1.5rem; + margin-left: 0.5rem; + position: relative; + width: 3rem; +} + +.loginControls-toggle-indicator { + align-items: center; + background: var(--bg-hl); + border: 1px solid var(--fg); + border-radius: 1rem; + bottom: 0.1rem; + display: flex; + height: 1.25rem; + justify-content: center; + left: 0.1rem; + outline: solid 2px transparent; + position: absolute; + transition: 0.25s; + width: 1.25rem; +} + +.loginControls-toggle-checkMark { + fill: var(--fg); + height: 1rem; + width: 1rem; + opacity: 0; + transition: opacity 0.25s ease-in-out; +} + +.loginControls-toggle-checkbox:checked + + .loginControls-toggle-track + .loginControls-toggle-indicator { + transform: translateX(1.4rem); +} +.loginControls-toggle-checkbox:checked + + .loginControls-toggle-track + .loginControls-toggle-indicator + .loginControls-toggle-checkMark { + opacity: 1; + transition: opacity 0.25s ease-in-out; +} + +.loginControls-submitButton { + margin-left: 0.5rem; +} + +.menu { + background-color: var(--bg-al); + backdrop-filter: blur(5px) saturate(70%); + left: 0; + padding: 0.5rem; + position: fixed; + bottom: 0; + width: 100%; +} + +.popupMenu { + background-color: var(--bg-hl); + border: 1px solid var(--fg); + border-radius: 0.5rem; + display: flex; + flex-direction: column; + justify-content: space-around; + right: -10rem; + padding: 0.5rem; + position: fixed; + top: -14rem; + transition: all 0.5s; + width: 5rem; +} + +.popupMenu > * { + margin-bottom: 0.5rem; +} + +.popupMenu > *:last-child { + margin-bottom: 0; +} + +.popupMenu-appInfo { + margin-top: 0; +} + +.toast { + display: flex; + align-items: center; + padding: 10px 20px; + background-color: #4caf50; + color: #ffffff; + border-radius: 5px; + font-size: 16px; + animation: + slideIn 0.5s, + fadeOut 0.5s 3s; + opacity: 1; +} + +.toast.error { + background-color: #f44336; +} + +.toastContainer { + position: fixed; + bottom: 8rem; + right: 2rem; + display: flex; + flex-direction: column; + gap: 1rem; + z-index: 1000; +} + +.twtControls { + align-items: center; + display: flex; + flex-direction: row; + max-height: 0; + overflow: hidden; + position: relative; +} + +.twtControls-appAuthor { + display: none; +} + +.twtControls-appInfo { + display: none; +} + +.twtControls-contentInput { + width: 100%; + min-height: 5rem; + overflow-y: auto; + resize: none; + transition: max-height 0.2s ease; +} + +.twtControls-contentLabel { + align-items: center; + display: flex; + flex-direction: row; + grid-area: textarea; + width: 100%; +} + +.twtControls-form { + width: 100%; +} + +.twtControls-formRow { + align-items: center; + display: grid; + grid-gap: 0.5rem; + grid-template-areas: + 'textarea menuButton' + 'textarea postButton'; + grid-template-columns: 1fr 3rem; + grid-template-rows: auto; + justify-content: space-between; +} + +.twtControls-gitLink { + display: none; +} + +.twtControls-uploadInputLabel { + align-items: center; + cursor: pointer; + display: flex; + justify-content: center; + max-width: 100%; + text-align: center; + transition: all 0.5s; +} + +.twtControls-uploadInputLabel-normal { + display: none; +} + +.twtControls-uploadInputLabel-small { + font-size: small; +} + +.twtControls-uploadInputLabel:hover { + background-color: var(--bg-hl); + color: var(--fg-hl); +} + +.twtControls-uploadInput { + display: none; +} + +.twtControls-submitButton { + background-color: var(--bg-hl); + border: 1px solid var(--link); + border-radius: 0.5rem; + color: var(--link); + grid-area: postButton; + height: 3rem; + width: 3rem; +} + +.twtxtEditForm { + max-height: 0; + overflow: hidden; + transition: all 0.5s; + width: 100%; +} + +.twtxtEditForm-controls { + display: flex; + gap: 0.5rem; + justify-content: end; + margin-top: 1rem; +} + +.twtxtEditForm-textarea { + font-size: large; + height: 80vh; + min-height: 6rem; + white-space: nowrap; + width: 100%; +} + +/* Media-based Overrides */ + +@media (min-width: 24rem) { + .loginControls-input { + max-width: 10rem; + } +} + +@media (min-width: 28rem) { + .loginControls-input { + max-width: 12rem; + } +} + +@media (min-width: 600px) { + #hamburgerToggleCheckbox:checked ~ .popupMenu { + left: 0.5rem; + } + + .popupMenu { + left: -10rem; + top: -6.5rem; + } + + .twtControls { + flex-direction: row; + } + + .twtControls-appInfo { + display: flex; + flex-direction: column; + grid-area: appInfo; + } + + .twtControls-submitButton { + justify-self: end; + } + + .twtControls-formRow { + grid-template-areas: 'menuButton appInfo textarea postButton'; + grid-template-columns: 3.5rem 3.5rem 1fr 3.5rem; + grid-template-rows: auto; + } +} + +@media (min-width: 900px) { + .popupMenu-appInfo { + display: none; + } + + .twtControls-appAuthor { + display: inline-block; + max-width: 20rem; + } + + .twtControls-appInfo { + display: block; + grid-area: appInfo; + } + + .twtControls-contentInput { + min-height: 5rem; + } + + .twtControls-formRow { + grid-template-areas: 'menuButton appInfo uploadButton textarea postButton'; + grid-template-columns: 3.5rem 1fr 5rem 1fr 3.5rem; + grid-template-rows: auto; + } + + .twtControls-gitLink { + display: block; + } + + .twtControls-uploadInputLabel-normal { + display: block; + grid-area: uploadButton; + margin-right: 0.5rem; + } + + .twtControls-uploadInputLabel-small { + display: none; + } +} + +@media (min-width: 1200px) { + .fileContentsSection { + margin-bottom: 1rem; + margin-top: 6rem; + } + + .menu { + top: 0; + bottom: auto; + } + + .popupMenu { + left: -10rem; + top: 5rem; + } + + .toastContainer { + bottom: 2rem; + } +} + +@media screen and (-ms-high-contrast: active) { + .loginControls-toggle-track { + border-radius: 0; + } +} + +/* State-Based Overrides */ + +.js-authorized .loginControls { + max-height: 0; +} + +.js-authorized .twtControls { + max-height: 100rem; +} + +.js-editMode .fileContentsSection-fileBox { + max-height: 0; + overflow: hidden; + transition: all 0.5s; +} + +.js-editMode .fileContentsSection-twtxtEditForm { + max-height: 100rem; +} + +/* TODO: Fix */ +@media (prefers-color-scheme: dark) { + :root { + /* #6e6e81 / Storm Gray */ + --bg: #ccc; + + --bg-al: rgb(204, 204, 204, 0.6); + + --bg-hl: #b6b6c0; + + /* #0a0a14 / Vulcan*/ + --fg: rgb(10, 10, 20); + + /* #1b1b27 / Steel Gray */ + --fg-hl: rgb(27, 27, 39); + + --link: #35353e; + } +} + +/* Animations */ + +@keyframes slideIn { + from { + transform: translateX(100%); + } + + to { + transform: translateX(0); + } +} + +@keyframes fadeOut { + from { + opacity: 1; + } + + to { + opacity: 0; + } +} diff --git a/dist/src/index.d.ts b/dist/src/index.d.ts new file mode 100644 index 0000000..036faae --- /dev/null +++ b/dist/src/index.d.ts @@ -0,0 +1 @@ +export { default } from "./plugin.js"; diff --git a/dist/src/index.js b/dist/src/index.js new file mode 100644 index 0000000..9fb2732 --- /dev/null +++ b/dist/src/index.js @@ -0,0 +1,2 @@ +export { default } from "./plugin.js"; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/dist/src/index.js.map b/dist/src/index.js.map new file mode 100644 index 0000000..8c8755f --- /dev/null +++ b/dist/src/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC"} \ No newline at end of file diff --git a/dist/src/lib/arrayDB.d.ts b/dist/src/lib/arrayDB.d.ts new file mode 100644 index 0000000..80dd2e9 --- /dev/null +++ b/dist/src/lib/arrayDB.d.ts @@ -0,0 +1,14 @@ +/** + * File-backed, in-memory database consisting of arrays of strings (ex: tokens) indexed by other + * strings (ex: usernames). + * + * @param name + * @param directory + * @returns + */ +export default function arrayDB(name: string, directory: string): Promise<{ + get: (key?: string) => string[]; + getObject: () => Record; + remove: (key?: string) => void; + set: (key?: string, value?: string[]) => string[]; +}>; diff --git a/dist/src/lib/arrayDB.js b/dist/src/lib/arrayDB.js new file mode 100644 index 0000000..81bd7ae --- /dev/null +++ b/dist/src/lib/arrayDB.js @@ -0,0 +1,73 @@ +import Debug from 'debug'; +import { join } from 'node:path'; +import { loadObjectFromJson, saveToJson } from './utils.js'; +const debug = Debug('twtkpr:arrayDB'); +/** + * File-backed, in-memory database consisting of arrays of strings (ex: tokens) indexed by other + * strings (ex: usernames). + * + * @param name + * @param directory + * @returns + */ +export default async function arrayDB(name, directory) { + let theName; + let dataObject; + const get = (key = '') => { + debug('get', { key }); + if (!theName || !dataObject) + throw new Error('DB must be initialized first'); + key = key?.trim(); + if (!key) + throw new Error('a valid key must be provided'); + return dataObject[key]; + }; + const getObject = () => dataObject; + const initialize = async (dbName = '') => { + debug('initialize starting', { dbName }); + dbName = dbName?.trim(); + if (!dbName) + throw new Error('a valid name must be provided'); + try { + dataObject = await loadObjectFromJson(join(directory, `${dbName}.json`)); + } + catch (err) { + debug('initialize read error', { err }); + if (err.code === 'ENOENT') + dataObject = {}; + else + throw err; + } + // only initialize (and set name) if everything passes + theName = dbName; + debug('initialize complete', { dataObject, name: theName }); + }; + const remove = (key = '') => { + debug('remove', { key }); + if (!theName || !dataObject) + throw new Error('DB must be initialized first'); + key = key?.trim(); + if (!key) + throw new Error('a valid key must be provided'); + delete dataObject[key]; + }; + const set = (key = '', value = []) => { + debug('set', { key }); + if (!theName || !dataObject) + throw new Error('DB must be initialized first'); + key = key?.trim(); + if (!key) + throw new Error('a valid key must be provided'); + dataObject[key] = value; + saveToJson(dataObject, join(directory, `${name}.json`)); + return value; + }; + await initialize(name); + return { + get, + getObject, + remove, + set, + }; +} +//# sourceMappingURL=arrayDB.js.map \ No newline at end of file diff --git a/dist/src/lib/arrayDB.js.map b/dist/src/lib/arrayDB.js.map new file mode 100644 index 0000000..59836d9 --- /dev/null +++ b/dist/src/lib/arrayDB.js.map @@ -0,0 +1 @@ +{"version":3,"file":"arrayDB.js","sourceRoot":"","sources":["../../../src/lib/arrayDB.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/src/lib/constants.d.ts b/dist/src/lib/constants.d.ts new file mode 100644 index 0000000..cfa2d19 --- /dev/null +++ b/dist/src/lib/constants.d.ts @@ -0,0 +1,20 @@ +export declare const DEFAULT_PRIVATE_DIRECTORY = ".data"; +export declare const DEFAULT_PUBLIC_DIRECTORY = "public"; +export declare const DEFAULT_TWTXT_FILENAME = "twtxt.txt"; +export declare const DEFAULT_ROUTE = "/twtxt.txt"; +export declare const DEFAULT_POST_LIMITER_ACTIVE = true; +export declare const DEFAULT_QUERY_PARAMETER_APP = "app"; +export declare const DEFAULT_QUERY_PARAMETER_CSS = "css"; +export declare const DEFAULT_QUERY_PARAMETER_FOLLOWING = "following"; +export declare const DEFAULT_QUERY_PARAMETER_JS = "js"; +export declare const DEFAULT_QUERY_PARAMETER_LOGOUT = "logout"; +export declare const DEFAULT_QUERY_PARAMETER_METADATA = "metadata"; +export declare const DEFAULT_QUERY_PARAMETER_TWT = "twt"; +export declare const DEFAULT_QUERY_PARAMETER_TWTS = "twts"; +export declare const DEFAULT_UPLOAD_ACTIVE = true; +export declare const DEFAULT_UPLOAD_ALLOWED_MIME_TYPES = ""; +export declare const DEFAULT_UPLOAD_ROUTE = "files"; +export declare const DEFAULT_UPLOAD_ENCODING = "utf-8"; +export declare const DEFAULT_UPLOAD_HASH_ALGORITHM = "sha256"; +export declare const DEFAULT_UPLOAD_KEEP_EXTENSIONS = true; +export declare const __dirname: string; diff --git a/dist/src/lib/constants.js b/dist/src/lib/constants.js new file mode 100644 index 0000000..6b88f5f --- /dev/null +++ b/dist/src/lib/constants.js @@ -0,0 +1,24 @@ +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +export const DEFAULT_PRIVATE_DIRECTORY = '.data'; +export const DEFAULT_PUBLIC_DIRECTORY = 'public'; +export const DEFAULT_TWTXT_FILENAME = 'twtxt.txt'; +export const DEFAULT_ROUTE = `/${DEFAULT_TWTXT_FILENAME}`; +export const DEFAULT_POST_LIMITER_ACTIVE = true; +export const DEFAULT_QUERY_PARAMETER_APP = 'app'; +export const DEFAULT_QUERY_PARAMETER_CSS = 'css'; +export const DEFAULT_QUERY_PARAMETER_FOLLOWING = 'following'; +export const DEFAULT_QUERY_PARAMETER_JS = 'js'; +export const DEFAULT_QUERY_PARAMETER_LOGOUT = 'logout'; +export const DEFAULT_QUERY_PARAMETER_METADATA = 'metadata'; +export const DEFAULT_QUERY_PARAMETER_TWT = 'twt'; +export const DEFAULT_QUERY_PARAMETER_TWTS = 'twts'; +export const DEFAULT_UPLOAD_ACTIVE = true; +export const DEFAULT_UPLOAD_ALLOWED_MIME_TYPES = ''; +export const DEFAULT_UPLOAD_ROUTE = 'files'; +export const DEFAULT_UPLOAD_ENCODING = 'utf-8'; +// optional in zod +export const DEFAULT_UPLOAD_HASH_ALGORITHM = 'sha256'; +export const DEFAULT_UPLOAD_KEEP_EXTENSIONS = true; +export const __dirname = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +//# sourceMappingURL=constants.js.map \ No newline at end of file diff --git a/dist/src/lib/constants.js.map b/dist/src/lib/constants.js.map new file mode 100644 index 0000000..58de6cd --- /dev/null +++ b/dist/src/lib/constants.js.map @@ -0,0 +1 @@ +{"version":3,"file":"constants.js","sourceRoot":"","sources":["../../../src/lib/constants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,CAAC,MAAM,yBAAyB,GAAG,OAAO,CAAC;AACjD,MAAM,CAAC,MAAM,wBAAwB,GAAG,QAAQ,CAAC;AACjD,MAAM,CAAC,MAAM,sBAAsB,GAAG,WAAW,CAAC;AAClD,MAAM,CAAC,MAAM,aAAa,GAAG,IAAI,sBAAsB,EAAE,CAAC;AAE1D,MAAM,CAAC,MAAM,2BAA2B,GAAG,IAAI,CAAC;AAEhD,MAAM,CAAC,MAAM,2BAA2B,GAAG,KAAK,CAAC;AACjD,MAAM,CAAC,MAAM,2BAA2B,GAAG,KAAK,CAAC;AACjD,MAAM,CAAC,MAAM,iCAAiC,GAAG,WAAW,CAAC;AAC7D,MAAM,CAAC,MAAM,0BAA0B,GAAG,IAAI,CAAC;AAC/C,MAAM,CAAC,MAAM,8BAA8B,GAAG,QAAQ,CAAC;AACvD,MAAM,CAAC,MAAM,gCAAgC,GAAG,UAAU,CAAC;AAC3D,MAAM,CAAC,MAAM,2BAA2B,GAAG,KAAK,CAAC;AACjD,MAAM,CAAC,MAAM,4BAA4B,GAAG,MAAM,CAAC;AAEnD,MAAM,CAAC,MAAM,qBAAqB,GAAG,IAAI,CAAC;AAC1C,MAAM,CAAC,MAAM,iCAAiC,GAAG,EAAE,CAAC;AACpD,MAAM,CAAC,MAAM,oBAAoB,GAAG,OAAO,CAAC;AAC5C,MAAM,CAAC,MAAM,uBAAuB,GAAG,OAAO,CAAC;AAE/C,kBAAkB;AAClB,MAAM,CAAC,MAAM,6BAA6B,GAAG,QAAQ,CAAC;AACtD,MAAM,CAAC,MAAM,8BAA8B,GAAG,IAAI,CAAC;AAEnD,MAAM,CAAC,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC"} \ No newline at end of file diff --git a/dist/src/lib/env.d.ts b/dist/src/lib/env.d.ts new file mode 100644 index 0000000..a1515b8 --- /dev/null +++ b/dist/src/lib/env.d.ts @@ -0,0 +1,56 @@ +import { z } from 'zod/v4'; +export declare const env: { + NODE_ENV: "development" | "production" | "test"; + TWTKPR_REFRESH_SECRET: string; + TWTKPR_ACCESS_SECRET: string; + TWTKPR_DEFAULT_ROUTE: string; + TWTKPR_PRIVATE_DIRECTORY: string; + TWTKPR_PUBLIC_DIRECTORY: string; + TWTKPR_QUERY_PARAMETER_APP: string; + TWTKPR_QUERY_PARAMETER_CSS: string; + TWTKPR_QUERY_PARAMETER_FOLLOWING: string; + TWTKPR_QUERY_PARAMETER_JS: string; + TWTKPR_QUERY_PARAMETER_LOGOUT: string; + TWTKPR_QUERY_PARAMETER_METADATA: string; + TWTKPR_QUERY_PARAMETER_TWT: string; + TWTKPR_QUERY_PARAMETER_TWTS: string; + TWTKPR_TWTXT_FILENAME: string; + TWTKPR_POST_LIMITER_ACTIVE: boolean; + TWTKPR_UPLOAD_ACTIVE: boolean; + TWTKPR_UPLOAD_ALLOWED_MIME_TYPES: string | string[]; + TWTKPR_UPLOAD_ROUTE: string; + TWTKPR_UPLOAD_ENCODING: string; + TWTKPR_UPLOAD_HASH_ALGORITHM: string | boolean; + TWTKPR_UPLOAD_KEEP_EXTENSIONS: boolean; + TWTKPR_POST_LIMITER_WINDOW_MS?: number | undefined; + TWTKPR_POST_LIMITER_LIMIT?: number | z.core.$InferOuterFunctionType | undefined; + TWTKPR_POST_LIMITER_MESSAGE?: any; + TWTKPR_POST_LIMITER_STATUS_CODE?: number | undefined; + TWTKPR_POST_LIMITER_HANDLER?: z.core.$InferOuterFunctionType | undefined; + TWTKPR_POST_LIMITER_LEGACY_HEADERS?: boolean | undefined; + TWTKPR_POST_LIMITER_STANDARD_HEADERS?: string | boolean | undefined; + TWTKPR_POST_LIMITER_IDENTIFIER?: string | z.core.$InferOuterFunctionType | undefined; + TWTKPR_POST_LIMITER_STORE?: any; + TWTKPR_POST_LIMITER_PASS_ON_STORE_ERROR?: boolean | undefined; + TWTKPR_POST_LIMITER_KEY_GENERATOR?: z.core.$InferOuterFunctionType | undefined; + TWTKPR_POST_LIMITER_IPV6_SUBNET?: number | boolean | z.core.$InferOuterFunctionType | undefined; + TWTKPR_POST_LIMITER_REQUEST_PROPERTY_NAME?: string | undefined; + TWTKPR_POST_LIMITER_SKIP?: z.core.$InferOuterFunctionType | undefined; + TWTKPR_POST_LIMITER_SKIP_SUCCESSFUL_REQUESTS?: boolean | undefined; + TWTKPR_POST_LIMITER_SKIP_FAILED_REQUESTS?: boolean | undefined; + TWTKPR_POST_LIMITER_REQUEST_WAS_SUCCESSFUL?: z.core.$InferOuterFunctionType | undefined; + TWTKPR_POST_LIMITER_VALIDATE?: boolean | Record | undefined; + TWTKPR_UPLOAD_ALLOW_EMPTY_FILES?: boolean | undefined; + TWTKPR_UPLOAD_CREATE_DIRS_FROM_UPLOADS?: boolean | undefined; + TWTKPR_UPLOAD_DIRECTORY?: string | undefined; + TWTKPR_UPLOAD_FILE_WRITE_STREAM_HANDLER?: z.core.$InferOuterFunctionType | undefined; + TWTKPR_UPLOAD_FILENAME?: z.core.$InferOuterFunctionType | undefined; + TWTKPR_UPLOAD_FILTER?: z.core.$InferOuterFunctionType | undefined; + TWTKPR_UPLOAD_MAX_FIELDS?: number | undefined; + TWTKPR_UPLOAD_MAX_FIELDS_SIZE?: number | undefined; + TWTKPR_UPLOAD_MAX_FILE_SIZE?: number | undefined; + TWTKPR_UPLOAD_MAX_FILES?: number | undefined; + TWTKPR_UPLOAD_MAX_TOTAL_FILE_SIZE?: number | undefined; + TWTKPR_UPLOAD_MIN_FILE_SIZE?: number | undefined; +}; +export declare const __dirname: string; diff --git a/dist/src/lib/env.js b/dist/src/lib/env.js new file mode 100644 index 0000000..070389a --- /dev/null +++ b/dist/src/lib/env.js @@ -0,0 +1,206 @@ +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { z } from 'zod/v4'; +import { DEFAULT_POST_LIMITER_ACTIVE, DEFAULT_PRIVATE_DIRECTORY, DEFAULT_PUBLIC_DIRECTORY, DEFAULT_QUERY_PARAMETER_APP, DEFAULT_QUERY_PARAMETER_CSS, DEFAULT_QUERY_PARAMETER_FOLLOWING, DEFAULT_QUERY_PARAMETER_JS, DEFAULT_QUERY_PARAMETER_LOGOUT, DEFAULT_QUERY_PARAMETER_METADATA, DEFAULT_QUERY_PARAMETER_TWT, DEFAULT_QUERY_PARAMETER_TWTS, DEFAULT_ROUTE, DEFAULT_TWTXT_FILENAME, DEFAULT_UPLOAD_ACTIVE, DEFAULT_UPLOAD_ALLOWED_MIME_TYPES, DEFAULT_UPLOAD_ENCODING, DEFAULT_UPLOAD_HASH_ALGORITHM, DEFAULT_UPLOAD_KEEP_EXTENSIONS, DEFAULT_UPLOAD_ROUTE, } from './constants.js'; +/* + The following keys are expected to exist in `process.env`, either as listed, or without the + `TWTKPR_` prefix + + We only have listed default values for our keys, anything for other plugins (like formidable or + express-rate-limit) fall back to their own defaults (and thus are optional). +*/ +const envSchema = z.object({ + NODE_ENV: z + .enum(['development', 'production', 'test']) + .default('development'), + // required vars - MUST be passed via ENV + TWTKPR_REFRESH_SECRET: z.string().default(''), + TWTKPR_ACCESS_SECRET: z.string().default(''), + // vars with default values + TWTKPR_DEFAULT_ROUTE: z.string().default(DEFAULT_ROUTE), + TWTKPR_PRIVATE_DIRECTORY: z.string().default(DEFAULT_PRIVATE_DIRECTORY), + TWTKPR_PUBLIC_DIRECTORY: z.string().default(DEFAULT_PUBLIC_DIRECTORY), + TWTKPR_QUERY_PARAMETER_APP: z.string().default(DEFAULT_QUERY_PARAMETER_APP), + TWTKPR_QUERY_PARAMETER_CSS: z.string().default(DEFAULT_QUERY_PARAMETER_CSS), + TWTKPR_QUERY_PARAMETER_FOLLOWING: z + .string() + .default(DEFAULT_QUERY_PARAMETER_FOLLOWING), + TWTKPR_QUERY_PARAMETER_JS: z.string().default(DEFAULT_QUERY_PARAMETER_JS), + TWTKPR_QUERY_PARAMETER_LOGOUT: z + .string() + .default(DEFAULT_QUERY_PARAMETER_LOGOUT), + TWTKPR_QUERY_PARAMETER_METADATA: z + .string() + .default(DEFAULT_QUERY_PARAMETER_METADATA), + TWTKPR_QUERY_PARAMETER_TWT: z.string().default(DEFAULT_QUERY_PARAMETER_TWT), + TWTKPR_QUERY_PARAMETER_TWTS: z.string().default(DEFAULT_QUERY_PARAMETER_TWTS), + TWTKPR_TWTXT_FILENAME: z.string().default(DEFAULT_TWTXT_FILENAME), + /** + * Post limiter plugin + */ + // var with default value + TWTKPR_POST_LIMITER_ACTIVE: z.boolean().default(DEFAULT_POST_LIMITER_ACTIVE), + // optional vars + TWTKPR_POST_LIMITER_WINDOW_MS: z.optional(z.number()), + TWTKPR_POST_LIMITER_LIMIT: z.optional(z.union([z.number(), z.function()])), + TWTKPR_POST_LIMITER_MESSAGE: z.optional(z.any()), + TWTKPR_POST_LIMITER_STATUS_CODE: z.optional(z.number()), + TWTKPR_POST_LIMITER_HANDLER: z.optional(z.function()), + TWTKPR_POST_LIMITER_LEGACY_HEADERS: z.optional(z.boolean()), + TWTKPR_POST_LIMITER_STANDARD_HEADERS: z.optional(z.union([z.boolean(), z.string()])), + TWTKPR_POST_LIMITER_IDENTIFIER: z.optional(z.union([z.string(), z.function()])), + TWTKPR_POST_LIMITER_STORE: z.optional(z.any()), + TWTKPR_POST_LIMITER_PASS_ON_STORE_ERROR: z.optional(z.boolean()), + TWTKPR_POST_LIMITER_KEY_GENERATOR: z.optional(z.function()), + TWTKPR_POST_LIMITER_IPV6_SUBNET: z.optional(z.union([z.number(), z.function(), z.boolean()])), + TWTKPR_POST_LIMITER_REQUEST_PROPERTY_NAME: z.optional(z.string()), + TWTKPR_POST_LIMITER_SKIP: z.optional(z.function()), + TWTKPR_POST_LIMITER_SKIP_SUCCESSFUL_REQUESTS: z.optional(z.boolean()), + TWTKPR_POST_LIMITER_SKIP_FAILED_REQUESTS: z.optional(z.boolean()), + TWTKPR_POST_LIMITER_REQUEST_WAS_SUCCESSFUL: z.optional(z.function()), + TWTKPR_POST_LIMITER_VALIDATE: z.optional(z.union([z.boolean(), z.object()])), + /** + * Upload plugin + */ + // vars with default values + TWTKPR_UPLOAD_ACTIVE: z.boolean().default(DEFAULT_UPLOAD_ACTIVE), + TWTKPR_UPLOAD_ALLOWED_MIME_TYPES: z + .union([z.string(), z.array(z.string())]) + .default(DEFAULT_UPLOAD_ALLOWED_MIME_TYPES), + TWTKPR_UPLOAD_ROUTE: z.string().default(DEFAULT_UPLOAD_ROUTE), + // optional vars + TWTKPR_UPLOAD_ALLOW_EMPTY_FILES: z.optional(z.boolean()), + TWTKPR_UPLOAD_CREATE_DIRS_FROM_UPLOADS: z.optional(z.boolean()), + TWTKPR_UPLOAD_DIRECTORY: z.optional(z.string()), + TWTKPR_UPLOAD_ENCODING: z.string().default(DEFAULT_UPLOAD_ENCODING), + TWTKPR_UPLOAD_FILE_WRITE_STREAM_HANDLER: z.optional(z.function()), + TWTKPR_UPLOAD_FILENAME: z.optional(z.function()), + TWTKPR_UPLOAD_FILTER: z.optional(z.function()), + TWTKPR_UPLOAD_HASH_ALGORITHM: z + .union([z.boolean(), z.string()]) + .default(DEFAULT_UPLOAD_HASH_ALGORITHM), + TWTKPR_UPLOAD_KEEP_EXTENSIONS: z + .boolean() + .default(DEFAULT_UPLOAD_KEEP_EXTENSIONS), + TWTKPR_UPLOAD_MAX_FIELDS: z.optional(z.number()), + TWTKPR_UPLOAD_MAX_FIELDS_SIZE: z.optional(z.number()), + TWTKPR_UPLOAD_MAX_FILE_SIZE: z.optional(z.number()), + TWTKPR_UPLOAD_MAX_FILES: z.optional(z.number()), + TWTKPR_UPLOAD_MAX_TOTAL_FILE_SIZE: z.optional(z.number()), + TWTKPR_UPLOAD_MIN_FILE_SIZE: z.optional(z.number()), +}); +const parseEnv = () => { + try { + /** + * there's probably an easier way to do this, spreading the keys from above and accepting either + * the app (bare) or library (`TWTKPR_`-prefixed) version of said key. + * But this should work for now. + */ + const parsedEnv = envSchema.parse({ + NODE_ENV: process.env.TWTKPR_NODE_ENV || process.env.NODE_ENV, + TWTKPR_ACCESS_SECRET: process.env.TWTKPR_ACCESS_SECRET || process.env.ACCESS_SECRET, + TWTKPR_DEFAULT_ROUTE: process.env.TWTKPR_DEFAULT_ROUTE || process.env.DEFAULT_ROUTE, + TWTKPR_PRIVATE_DIRECTORY: process.env.TWTKPR_PRIVATE_DIRECTORY || process.env.PRIVATE_DIRECTORY, + TWTKPR_PUBLIC_DIRECTORY: process.env.TWTKPR_PUBLIC_DIRECTORY || process.env.PUBLIC_DIRECTORY, + TWTKPR_REFRESH_SECRET: process.env.TWTKPR_REFRESH_SECRET || process.env.REFRESH_SECRET, + TWTKPR_TWTXT_FILENAME: process.env.TWTKPR_TWTXT_FILENAME || process.env.TWTXT_FILENAME, + TWTKPR_POST_LIMITER_ACTIVE: process.env.TWTKPR_POST_LIMITER_ACTIVE || + process.env.POST_LIMITER_ACTIVE, + TWTKPR_POST_LIMITER_WINDOW_MS: process.env.TWTKPR_POST_LIMITER_WINDOW_MS || + process.env.POST_LIMITER_WINDOW_MS, + TWTKPR_POST_LIMITER_LIMIT: process.env.TWTKPR_POST_LIMITER_LIMIT || process.env.POST_LIMITER_LIMIT, + TWTKPR_POST_LIMITER_MESSAGE: process.env.TWTKPR_POST_LIMITER_MESSAGE || + process.env.POST_LIMITER_MESSAGE, + TWTKPR_POST_LIMITER_STATUS_CODE: process.env.TWTKPR_POST_LIMITER_STATUS_CODE || + process.env.POST_LIMITER_STATUS_CODE, + TWTKPR_POST_LIMITER_HANDLER: process.env.TWTKPR_POST_LIMITER_HANDLER || + process.env.POST_LIMITER_HANDLER, + TWTKPR_POST_LIMITER_LEGACY_HEADERS: process.env.TWTKPR_POST_LIMITER_LEGACY_HEADERS || + process.env.POST_LIMITER_LEGACY_HEADERS, + TWTKPR_POST_LIMITER_STANDARD_HEADERS: process.env.TWTKPR_POST_LIMITER_STANDARD_HEADERS || + process.env.POST_LIMITER_STANDARD_HEADERS, + TWTKPR_POST_LIMITER_IDENTIFIER: process.env.TWTKPR_POST_LIMITER_IDENTIFIER || + process.env.POST_LIMITER_IDENTIFIER, + TWTKPR_POST_LIMITER_STORE: process.env.TWTKPR_POST_LIMITER_STORE || process.env.POST_LIMITER_STORE, + TWTKPR_POST_LIMITER_PASS_ON_STORE_ERROR: process.env.TWTKPR_POST_LIMITER_PASS_ON_STORE_ERROR || + process.env.POST_LIMITER_PASS_ON_STORE_ERROR, + TWTKPR_POST_LIMITER_KEY_GENERATOR: process.env.TWTKPR_POST_LIMITER_KEY_GENERATOR || + process.env.POST_LIMITER_KEY_GENERATOR, + TWTKPR_POST_LIMITER_IPV6_SUBNET: process.env.TWTKPR_POST_LIMITER_IPV6_SUBNET || + process.env.POST_LIMITER_IPV6_SUBNET, + TWTKPR_POST_LIMITER_REQUEST_PROPERTY_NAME: process.env.TWTKPR_POST_LIMITER_REQUEST_PROPERTY_NAME || + process.env.POST_LIMITER_REQUEST_PROPERTY_NAME, + TWTKPR_POST_LIMITER_SKIP: process.env.TWTKPR_POST_LIMITER_SKIP || process.env.POST_LIMITER_SKIP, + TWTKPR_POST_LIMITER_SKIP_SUCCESSFUL_REQUESTS: process.env.TWTKPR_POST_LIMITER_SKIP_SUCCESSFUL_REQUESTS || + process.env.POST_LIMITER_SKIP_SUCCESSFUL_REQUESTS, + TWTKPR_POST_LIMITER_SKIP_FAILED_REQUESTS: process.env.TWTKPR_POST_LIMITER_SKIP_FAILED_REQUESTS || + process.env.POST_LIMITER_SKIP_FAILED_REQUESTS, + TWTKPR_POST_LIMITER_REQUEST_WAS_SUCCESSFUL: process.env.TWTKPR_POST_LIMITER_REQUEST_WAS_SUCCESSFUL || + process.env.POST_LIMITER_REQUEST_WAS_SUCCESSFUL, + TWTKPR_POST_LIMITER_VALIDATE: process.env.TWTKPR_POST_LIMITER_VALIDATE || + process.env.POST_LIMITER_VALIDATE, + TWTKPR_QUERY_PARAMETER_APP: process.env.TWTKPR_QUERY_PARAMETER_APP || + process.env.QUERY_PARAMETER_APP, + TWTKPR_QUERY_PARAMETER_CSS: process.env.TWTKPR_QUERY_PARAMETER_CSS || + process.env.QUERY_PARAMETER_CSS, + TWTKPR_QUERY_PARAMETER_FOLLOWING: process.env.TWTKPR_QUERY_PARAMETER_FOLLOWING || + process.env.QUERY_PARAMETER_FOLLOWING, + TWTKPR_QUERY_PARAMETER_JS: process.env.TWTKPR_QUERY_PARAMETER_JS || process.env.QUERY_PARAMETER_JS, + TWTKPR_QUERY_PARAMETER_LOGOUT: process.env.TWTKPR_QUERY_PARAMETER_LOGOUT || + process.env.QUERY_PARAMETER_LOGOUT, + TWTKPR_QUERY_PARAMETER_METADATA: process.env.TWTKPR_QUERY_PARAMETER_METADATA || + process.env.QUERY_PARAMETER_METADATA, + TWTKPR_QUERY_PARAMETER_TWT: process.env.TWTKPR_QUERY_PARAMETER_TWT || + process.env.QUERY_PARAMETER_TWT, + TWTKPR_QUERY_PARAMETER_TWTS: process.env.TWTKPR_QUERY_PARAMETER_TWTS || + process.env.QUERY_PARAMETER_TWTS, + TWTKPR_UPLOAD_ACTIVE: process.env.TWTKPR_UPLOAD_ACTIVE || process.env.UPLOAD_ACTIVE, + TWTKPR_UPLOAD_ALLOW_EMPTY_FILES: process.env.TWTKPR_UPLOAD_ALLOW_EMPTY_FILES || + process.env.UPLOAD_ALLOW_EMPTY_FILES, + TWTKPR_UPLOAD_ALLOWED_MIME_TYPES: process.env.TWTKPR_UPLOAD_ALLOWED_MIME_TYPES || + process.env.UPLOAD_ALLOWED_MIME_TYPES, + TWTKPR_UPLOAD_CREATE_DIRS_FROM_UPLOADS: process.env.TWTKPR_UPLOAD_CREATE_DIRS_FROM_UPLOADS || + process.env.UPLOAD_CREATE_DIRS_FROM_UPLOADS, + TWTKPR_UPLOAD_DIRECTORY: process.env.TWTKPR_UPLOAD_DIRECTORY || + process.env.UPLOAD_DIRECTORY || + process.env.UPLOAD_DIR, + TWTKPR_UPLOAD_ENCODING: process.env.TWTKPR_UPLOAD_ENCODING || process.env.UPLOAD_ENCODING, + TWTKPR_UPLOAD_FILE_WRITE_STREAM_HANDLER: process.env.TWTKPR_UPLOAD_FILE_WRITE_STREAM_HANDLER || + process.env.UPLOAD_FILE_WRITE_STREAM_HANDLER, + TWTKPR_UPLOAD_FILENAME: process.env.TWTKPR_UPLOAD_FILENAME || process.env.UPLOAD_FILENAME, + TWTKPR_UPLOAD_FILTER: process.env.TWTKPR_UPLOAD_FILTER || process.env.UPLOAD_FILTER, + TWTKPR_UPLOAD_HASH_ALGORITHM: process.env.TWTKPR_UPLOAD_HASH_ALGORITHM || + process.env.UPLOAD_HASH_ALGORITHM, + TWTKPR_UPLOAD_KEEP_EXTENSIONS: process.env.TWTKPR_UPLOAD_KEEP_EXTENSIONS || + process.env.UPLOAD_KEEP_EXTENSIONS, + TWTKPR_UPLOAD_MAX_FIELDS: process.env.TWTKPR_UPLOAD_MAX_FIELDS || process.env.UPLOAD_MAX_FIELDS, + TWTKPR_UPLOAD_MAX_FIELDS_SIZE: process.env.TWTKPR_UPLOAD_MAX_FIELDS_SIZE || + process.env.UPLOAD_MAX_FIELDS_SIZE, + TWTKPR_UPLOAD_MAX_FILE_SIZE: process.env.TWTKPR_UPLOAD_MAX_FILE_SIZE || + process.env.UPLOAD_MAX_FILE_SIZE, + TWTKPR_UPLOAD_MAX_FILES: process.env.TWTKPR_UPLOAD_MAX_FILES || process.env.UPLOAD_MAX_FILES, + TWTKPR_UPLOAD_MAX_TOTAL_FILE_SIZE: process.env.TWTKPR_UPLOAD_MAX_TOTAL_FILE_SIZE || + process.env.UPLOAD_MAX_TOTAL_FILE_SIZE, + TWTKPR_UPLOAD_MIN_FILE_SIZE: process.env.TWTKPR_UPLOAD_MIN_FILE_SIZE || + process.env.UPLOAD_MIN_FILE_SIZE, + TWTKPR_UPLOAD_ROUTE: process.env.TWTKPR_UPLOAD_ROUTE || process.env.UPLOAD_ROUTE, + }); + if (!parsedEnv.TWTKPR_ACCESS_SECRET) + throw new Error('Either ACCESS_SECRET or TWTKPR_ACCESS_SECRET must be provided'); + if (!parsedEnv.TWTKPR_REFRESH_SECRET) + throw new Error('Either REFRESH_SECRET or TWTKPR_REFRESH_SECRET must be provided'); + return parsedEnv; + } + catch (error) { + if (error instanceof z.ZodError) { + console.error('Missing environment variables:', error.issues.flatMap((issue) => `${issue.path} or TWTKPR_${issue.path}`)); + } + else { + console.error(error); + } + process.exit(1); + } +}; +export const env = parseEnv(); +export const __dirname = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +//# sourceMappingURL=env.js.map \ No newline at end of file diff --git a/dist/src/lib/env.js.map b/dist/src/lib/env.js.map new file mode 100644 index 0000000..49b2b80 --- /dev/null +++ b/dist/src/lib/env.js.map @@ -0,0 +1 @@ +{"version":3,"file":"env.js","sourceRoot":"","sources":["../../../src/lib/env.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/src/lib/getConfiguration.d.ts b/dist/src/lib/getConfiguration.d.ts new file mode 100644 index 0000000..64e1b9f --- /dev/null +++ b/dist/src/lib/getConfiguration.d.ts @@ -0,0 +1,7 @@ +import { TwtKprConfiguration, TwtKprPluginConfiguration } from '../types.js'; +/** + * + * @param initialConfiguration + * @returns + */ +export default function getConfiguration(initialConfiguration: TwtKprPluginConfiguration): TwtKprConfiguration; diff --git a/dist/src/lib/getConfiguration.js b/dist/src/lib/getConfiguration.js new file mode 100644 index 0000000..92d343f --- /dev/null +++ b/dist/src/lib/getConfiguration.js @@ -0,0 +1,103 @@ +import { env } from './env.js'; +/** + * + * @param allowedMimeTypes + * @returns + */ +const getDestinationByMimeTypeConfiguration = (allowedMimeTypes) => { + const fallback = { + audio: { + directory: 'audio', + rename: false, + }, + image: { + directory: 'images', + rename: true, + }, + video: { + directory: 'videos', + rename: true, + }, + '*': { + directory: 'files', + rename: false, + }, + }; + const mimeTypeArrayReducer = (acc, curr) => { + if (fallback[curr]) + acc[curr] = fallback[curr]; + else + acc[curr] = { + directory: `${curr}s`, + rename: false, + }; + return acc; + }; + if (!allowedMimeTypes) + return fallback; + if (typeof allowedMimeTypes === 'string') + return allowedMimeTypes + .split(',') + .map((val) => val.trim()) + .reduce(mimeTypeArrayReducer, {}); + if (Array.isArray(allowedMimeTypes)) + return allowedMimeTypes.reduce(mimeTypeArrayReducer, {}); + if (typeof allowedMimeTypes === 'object') + return allowedMimeTypes; + return fallback; +}; +/** + * + * @param initialConfiguration + * @returns + */ +export default function getConfiguration(initialConfiguration) { + const { mainRoute = env.TWTKPR_DEFAULT_ROUTE, privateDirectory = env.TWTKPR_PRIVATE_DIRECTORY, publicDirectory = env.TWTKPR_PUBLIC_DIRECTORY, twtxtFilename = env.TWTKPR_TWTXT_FILENAME, postLimiterConfiguration, queryParameters, uploadConfiguration, } = initialConfiguration ?? {}; + const { active: postLimiterActive = env.TWTKPR_POST_LIMITER_ACTIVE, ...otherPostLimiterProps } = postLimiterConfiguration ?? {}; + const { app = env.TWTKPR_QUERY_PARAMETER_APP, css = env.TWTKPR_QUERY_PARAMETER_CSS, following = env.TWTKPR_QUERY_PARAMETER_FOLLOWING, js = env.TWTKPR_QUERY_PARAMETER_JS, logout = env.TWTKPR_QUERY_PARAMETER_LOGOUT, metadata = env.TWTKPR_QUERY_PARAMETER_METADATA, twt = env.TWTKPR_QUERY_PARAMETER_TWT, twts = env.TWTKPR_QUERY_PARAMETER_TWTS, } = queryParameters ?? {}; + const { active: uploadActive = env.TWTKPR_UPLOAD_ACTIVE, allowEmptyFiles = env.TWTKPR_UPLOAD_ALLOW_EMPTY_FILES, allowedMimeTypes = env.TWTKPR_UPLOAD_ALLOWED_MIME_TYPES, createDirsFromUploads = env.TWTKPR_UPLOAD_CREATE_DIRS_FROM_UPLOADS, directory = env.TWTKPR_UPLOAD_DIRECTORY, encoding = env.TWTKPR_UPLOAD_ENCODING, fileWriteStreamHandler, filter = () => true, hashAlgorithm = env.TWTKPR_UPLOAD_HASH_ALGORITHM, keepExtensions = env.TWTKPR_UPLOAD_KEEP_EXTENSIONS, maxFields = env.TWTKPR_UPLOAD_MAX_FIELDS, maxFileSize = env.TWTKPR_UPLOAD_MAX_FIELDS_SIZE, maxFiles = env.TWTKPR_UPLOAD_MAX_FILES, maxTotalFileSize = env.TWTKPR_UPLOAD_MAX_TOTAL_FILE_SIZE, minFileSize = env.TWTKPR_UPLOAD_MIN_FILE_SIZE, route = env.TWTKPR_UPLOAD_ROUTE, } = uploadConfiguration ?? {}; + return { + // secrets cannot be provided through configuration file, must use ENV / .env + accessSecret: env.TWTKPR_ACCESS_SECRET, + refreshSecret: env.TWTKPR_REFRESH_SECRET, + mainRoute, + privateDirectory, + publicDirectory, + twtxtFilename, + postLimiterConfiguration: { + active: postLimiterActive, + ...(otherPostLimiterProps ?? {}), + }, + queryParameters: { + ...queryParameters, + app, + css, + following, + js, + logout, + metadata, + twt, + twts, + }, + uploadConfiguration: { + ...uploadConfiguration, + active: uploadActive, + allowEmptyFiles, + allowedMimeTypes: getDestinationByMimeTypeConfiguration(allowedMimeTypes), + createDirsFromUploads, + directory, + encoding, + fileWriteStreamHandler, + filter, + hashAlgorithm: hashAlgorithm, + keepExtensions, + maxFields, + maxFileSize, + maxFiles, + maxTotalFileSize, + minFileSize, + route, + }, + }; +} +//# sourceMappingURL=getConfiguration.js.map \ No newline at end of file diff --git a/dist/src/lib/getConfiguration.js.map b/dist/src/lib/getConfiguration.js.map new file mode 100644 index 0000000..b8914df --- /dev/null +++ b/dist/src/lib/getConfiguration.js.map @@ -0,0 +1 @@ +{"version":3,"file":"getConfiguration.js","sourceRoot":"","sources":["../../../src/lib/getConfiguration.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAE/B;;;;GAIG;AACH,MAAM,qCAAqC,GAAG,CAC7C,gBAAkE,EACjE,EAAE;IACH,MAAM,QAAQ,GAAgC;QAC7C,KAAK,EAAE;YACN,SAAS,EAAE,OAAO;YAClB,MAAM,EAAE,KAAK;SACb;QACD,KAAK,EAAE;YACN,SAAS,EAAE,QAAQ;YACnB,MAAM,EAAE,IAAI;SACZ;QACD,KAAK,EAAE;YACN,SAAS,EAAE,QAAQ;YACnB,MAAM,EAAE,IAAI;SACZ;QACD,GAAG,EAAE;YACJ,SAAS,EAAE,OAAO;YAClB,MAAM,EAAE,KAAK;SACb;KACD,CAAC;IAEF,MAAM,oBAAoB,GAAG,CAC5B,GAAgC,EAChC,IAAY,EACX,EAAE;QACH,IAAI,QAAQ,CAAC,IAAI,CAAC;YAAE,GAAG,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;;YAE9C,GAAG,CAAC,IAAI,CAAC,GAAG;gBACX,SAAS,EAAE,GAAG,IAAI,GAAG;gBACrB,MAAM,EAAE,KAAK;aACb,CAAC;QAEH,OAAO,GAAG,CAAC;IACZ,CAAC,CAAC;IAEF,IAAI,CAAC,gBAAgB;QAAE,OAAO,QAAQ,CAAC;IAEvC,IAAI,OAAO,gBAAgB,KAAK,QAAQ;QACvC,OAAO,gBAAgB;aACrB,KAAK,CAAC,GAAG,CAAC;aACV,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;aACxB,MAAM,CAAC,oBAAoB,EAAE,EAAE,CAAC,CAAC;IAEpC,IAAI,KAAK,CAAC,OAAO,CAAC,gBAAgB,CAAC;QAClC,OAAQ,gBAA6B,CAAC,MAAM,CAAC,oBAAoB,EAAE,EAAE,CAAC,CAAC;IAExE,IAAI,OAAO,gBAAgB,KAAK,QAAQ;QAAE,OAAO,gBAAgB,CAAC;IAElE,OAAO,QAAQ,CAAC;AACjB,CAAC,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,OAAO,UAAU,gBAAgB,CACvC,oBAA+C;IAE/C,MAAM,EACL,SAAS,GAAG,GAAG,CAAC,oBAAoB,EACpC,gBAAgB,GAAG,GAAG,CAAC,wBAAwB,EAC/C,eAAe,GAAG,GAAG,CAAC,uBAAuB,EAC7C,aAAa,GAAG,GAAG,CAAC,qBAAqB,EACzC,wBAAwB,EACxB,eAAe,EACf,mBAAmB,GACnB,GAAG,oBAAoB,IAAI,EAAE,CAAC;IAE/B,MAAM,EACL,MAAM,EAAE,iBAAiB,GAAG,GAAG,CAAC,0BAA0B,EAC1D,GAAG,qBAAqB,EACxB,GAAG,wBAAwB,IAAI,EAAE,CAAC;IAEnC,MAAM,EACL,GAAG,GAAG,GAAG,CAAC,0BAA0B,EACpC,GAAG,GAAG,GAAG,CAAC,0BAA0B,EACpC,SAAS,GAAG,GAAG,CAAC,gCAAgC,EAChD,EAAE,GAAG,GAAG,CAAC,yBAAyB,EAClC,MAAM,GAAG,GAAG,CAAC,6BAA6B,EAC1C,QAAQ,GAAG,GAAG,CAAC,+BAA+B,EAC9C,GAAG,GAAG,GAAG,CAAC,0BAA0B,EACpC,IAAI,GAAG,GAAG,CAAC,2BAA2B,GACtC,GAAG,eAAe,IAAI,EAAE,CAAC;IAE1B,MAAM,EACL,MAAM,EAAE,YAAY,GAAG,GAAG,CAAC,oBAAoB,EAC/C,eAAe,GAAG,GAAG,CAAC,+BAA+B,EACrD,gBAAgB,GAAG,GAAG,CAAC,gCAAgC,EACvD,qBAAqB,GAAG,GAAG,CAAC,sCAAsC,EAClE,SAAS,GAAG,GAAG,CAAC,uBAAuB,EACvC,QAAQ,GAAG,GAAG,CAAC,sBAAsB,EACrC,sBAAsB,EACtB,MAAM,GAAG,GAAG,EAAE,CAAC,IAAI,EACnB,aAAa,GAAG,GAAG,CAAC,4BAA4B,EAChD,cAAc,GAAG,GAAG,CAAC,6BAA6B,EAClD,SAAS,GAAG,GAAG,CAAC,wBAAwB,EACxC,WAAW,GAAG,GAAG,CAAC,6BAA6B,EAC/C,QAAQ,GAAG,GAAG,CAAC,uBAAuB,EACtC,gBAAgB,GAAG,GAAG,CAAC,iCAAiC,EACxD,WAAW,GAAG,GAAG,CAAC,2BAA2B,EAC7C,KAAK,GAAG,GAAG,CAAC,mBAAmB,GAC/B,GAAG,mBAAmB,IAAI,EAAE,CAAC;IAE9B,OAAO;QACN,6EAA6E;QAC7E,YAAY,EAAE,GAAG,CAAC,oBAAoB;QACtC,aAAa,EAAE,GAAG,CAAC,qBAAqB;QACxC,SAAS;QACT,gBAAgB;QAChB,eAAe;QACf,aAAa;QACb,wBAAwB,EAAE;YACzB,MAAM,EAAE,iBAAiB;YACzB,GAAG,CAAC,qBAAqB,IAAI,EAAE,CAAC;SAChC;QACD,eAAe,EAAE;YAChB,GAAG,eAAe;YAClB,GAAG;YACH,GAAG;YACH,SAAS;YACT,EAAE;YACF,MAAM;YACN,QAAQ;YACR,GAAG;YACH,IAAI;SACJ;QACD,mBAAmB,EAAE;YACpB,GAAG,mBAAmB;YACtB,MAAM,EAAE,YAAY;YACpB,eAAe;YACf,gBAAgB,EAAE,qCAAqC,CAAC,gBAAgB,CAAC;YACzE,qBAAqB;YACrB,SAAS;YACT,QAAQ;YACR,sBAAsB;YACtB,MAAM;YACN,aAAa,EAAE,aAA2C;YAC1D,cAAc;YACd,SAAS;YACT,WAAW;YACX,QAAQ;YACR,gBAAgB;YAChB,WAAW;YACX,KAAK;SACL;KACsB,CAAC;AAC1B,CAAC"} \ No newline at end of file diff --git a/dist/src/lib/refreshTokensDB.d.ts b/dist/src/lib/refreshTokensDB.d.ts new file mode 100644 index 0000000..89ecf52 --- /dev/null +++ b/dist/src/lib/refreshTokensDB.d.ts @@ -0,0 +1,13 @@ +export interface RefreshTokensDB { + cleanUp: () => void; + get: (key: string) => string[]; + getObject: () => Record; + remove: (key?: string) => void; + set: (key?: string, value?: string[]) => string[]; +} +/** + * + * @param directory + * @returns + */ +export default function refreshTokensDB(directory: string): Promise; diff --git a/dist/src/lib/refreshTokensDB.js b/dist/src/lib/refreshTokensDB.js new file mode 100644 index 0000000..47b269d --- /dev/null +++ b/dist/src/lib/refreshTokensDB.js @@ -0,0 +1,40 @@ +import arrayDB from './arrayDB.js'; +import Debug from 'debug'; +import jwt from 'jsonwebtoken'; +const debug = Debug('twtkpr:simpleDB'); +/** + * + * @param directory + * @returns + */ +export default async function refreshTokensDB(directory) { + const refreshTokensDB = await arrayDB('refreshTokens', directory); + const get = (key) => { + const currentTime = Math.floor(Date.now() / 1000); + debug('get', key, currentTime); + return (refreshTokensDB.get(key) ?? []).filter((token) => { + const val = jwt.decode(token); + return val && (val.exp ?? 0) >= currentTime; + }); + }; + const cleanUp = () => { + const currentTime = Math.floor(Date.now() / 1000); + const tokenListByUserId = refreshTokensDB.getObject(); + debug('cleanup', currentTime); + Object.keys(tokenListByUserId).forEach((userId) => { + const tokens = refreshTokensDB.get(userId).filter((token) => { + const val = jwt.decode(token); + return val && (val.exp ?? 0) >= currentTime; + }); + debug(`setting tokens for ${userId}`, tokens); + refreshTokensDB.set(userId, tokens); + }); + }; + cleanUp(); + return { + ...refreshTokensDB, + cleanUp, + get, + }; +} +//# sourceMappingURL=refreshTokensDB.js.map \ No newline at end of file diff --git a/dist/src/lib/refreshTokensDB.js.map b/dist/src/lib/refreshTokensDB.js.map new file mode 100644 index 0000000..36c4c9a --- /dev/null +++ b/dist/src/lib/refreshTokensDB.js.map @@ -0,0 +1 @@ +{"version":3,"file":"refreshTokensDB.js","sourceRoot":"","sources":["../../../src/lib/refreshTokensDB.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,cAAc,CAAC;AACnC,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,GAAG,MAAM,cAAc,CAAC;AAU/B,MAAM,KAAK,GAAG,KAAK,CAAC,iBAAiB,CAAC,CAAC;AAEvC;;;;GAIG;AACH,MAAM,CAAC,OAAO,CAAC,KAAK,UAAU,eAAe,CAAC,SAAiB;IAC9D,MAAM,eAAe,GAAG,MAAM,OAAO,CAAC,eAAe,EAAE,SAAS,CAAC,CAAC;IAElE,MAAM,GAAG,GAAG,CAAC,GAAW,EAAE,EAAE;QAC3B,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;QAClD,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,WAAW,CAAC,CAAC;QAE/B,OAAO,CAAC,eAAe,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE;YACxD,MAAM,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YAC9B,OAAO,GAAG,IAAI,CAAE,GAAsB,CAAC,GAAG,IAAI,CAAC,CAAC,IAAI,WAAW,CAAC;QACjE,CAAC,CAAC,CAAC;IACJ,CAAC,CAAC;IAEF,MAAM,OAAO,GAAG,GAAG,EAAE;QACpB,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;QAClD,MAAM,iBAAiB,GAAG,eAAe,CAAC,SAAS,EAAE,CAAC;QAEtD,KAAK,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;QAE9B,MAAM,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE;YACjD,MAAM,MAAM,GAAG,eAAe,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE;gBAC3D,MAAM,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBAC9B,OAAO,GAAG,IAAI,CAAE,GAAsB,CAAC,GAAG,IAAI,CAAC,CAAC,IAAI,WAAW,CAAC;YACjE,CAAC,CAAC,CAAC;YAEH,KAAK,CAAC,sBAAsB,MAAM,EAAE,EAAE,MAAM,CAAC,CAAC;YAE9C,eAAe,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACrC,CAAC,CAAC,CAAC;IACJ,CAAC,CAAC;IAEF,OAAO,EAAE,CAAC;IAEV,OAAO;QACN,GAAG,eAAe;QAClB,OAAO;QACP,GAAG;KACgB,CAAC;AACtB,CAAC"} \ No newline at end of file diff --git a/dist/src/lib/simpleDB.d.ts b/dist/src/lib/simpleDB.d.ts new file mode 100644 index 0000000..f442a6c --- /dev/null +++ b/dist/src/lib/simpleDB.d.ts @@ -0,0 +1,12 @@ +/** + * + * @param name + * @param directory + * @returns + */ +export default function simpleDB(name: string, directory: string): Promise<{ + get: (key?: string) => string; + getObject: () => Record; + remove: (key?: string) => void; + set: (key?: string, value?: string) => string; +}>; diff --git a/dist/src/lib/simpleDB.js b/dist/src/lib/simpleDB.js new file mode 100644 index 0000000..191f215 --- /dev/null +++ b/dist/src/lib/simpleDB.js @@ -0,0 +1,71 @@ +import Debug from 'debug'; +import path from 'node:path'; +import { loadObjectFromJson, saveToJson } from './utils.js'; +const debug = Debug('twtkpr:simpleDB'); +/** + * + * @param name + * @param directory + * @returns + */ +export default async function simpleDB(name, directory) { + let theName; + let dataObject; + const get = (key = '') => { + debug('get', { key }); + if (!theName || !dataObject) + throw new Error('DB must be initialized first'); + key = key?.trim(); + if (!key) + throw new Error('a valid key must be provided'); + return dataObject[key]; + }; + const getObject = () => dataObject; + const initialize = async (dbName = '') => { + debug('initialize starting', { dbName }); + dbName = dbName?.trim(); + if (!dbName) + throw new Error('a valid name must be provided'); + try { + dataObject = await loadObjectFromJson(path.join(directory, `${dbName}.json`)); + } + catch (err) { + debug('initialize read error', { err }); + if (err.code === 'ENOENT') + dataObject = {}; + else + throw err; + } + // only initialize (and set name) if everything passes + theName = dbName; + debug('initialize complete', { dataObject, name: theName }); + }; + const remove = (key = '') => { + debug('remove', { key }); + if (!theName || !dataObject) + throw new Error('DB must be initialized first'); + key = key?.trim(); + if (!key) + throw new Error('a valid key must be provided'); + delete dataObject[key]; + }; + const set = (key = '', value = '') => { + debug('set', { key }); + if (!theName || !dataObject) + throw new Error('DB must be initialized first'); + key = key?.trim(); + if (!key) + throw new Error('a valid key must be provided'); + dataObject[key] = value; + saveToJson(dataObject, path.join(directory, `${name}.json`)); + return value; + }; + await initialize(name); + return { + get, + getObject, + remove, + set, + }; +} +//# sourceMappingURL=simpleDB.js.map \ No newline at end of file diff --git a/dist/src/lib/simpleDB.js.map b/dist/src/lib/simpleDB.js.map new file mode 100644 index 0000000..d638780 --- /dev/null +++ b/dist/src/lib/simpleDB.js.map @@ -0,0 +1 @@ +{"version":3,"file":"simpleDB.js","sourceRoot":"","sources":["../../../src/lib/simpleDB.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/src/lib/twtxtCache.d.ts b/dist/src/lib/twtxtCache.d.ts new file mode 100644 index 0000000..90a2665 --- /dev/null +++ b/dist/src/lib/twtxtCache.d.ts @@ -0,0 +1,11 @@ +import { NodeCache } from '@cacheable/node-cache'; +import { TwtKprConfiguration } from '../types.js'; +/** + * + * @param param0 + * @returns + */ +export default function twtxtCache({ publicDirectory, twtxtFilename, }: Pick): { + cache: NodeCache; + reloadCache: () => Promise; +}; diff --git a/dist/src/lib/twtxtCache.js b/dist/src/lib/twtxtCache.js new file mode 100644 index 0000000..541b6b2 --- /dev/null +++ b/dist/src/lib/twtxtCache.js @@ -0,0 +1,31 @@ +import fsp from 'node:fs/promises'; +import path from 'node:path'; +import { NodeCache } from '@cacheable/node-cache'; +import Debug from 'debug'; +import { parseTwtxt } from 'twtxt-lib'; +/** + * + * @param param0 + * @returns + */ +export default function twtxtCache({ publicDirectory, twtxtFilename, }) { + let isLoaded = false; + const debug = Debug('twtkpr:twtxtCache'); + const cache = new NodeCache(); + const reloadCache = async () => { + const fileText = await fsp.readFile(path.join(publicDirectory, twtxtFilename), 'utf8'); + const parsedFile = parseTwtxt(fileText); + Object.keys(parsedFile).forEach((key) => { + cache.set(key, parsedFile[key]); // 10 seconds + }); + cache.set('source', fileText); + debug(`cache ${isLoaded ? 're' : ''}loaded`); + isLoaded = true; + }; + reloadCache(); + return { + cache, + reloadCache, + }; +} +//# sourceMappingURL=twtxtCache.js.map \ No newline at end of file diff --git a/dist/src/lib/twtxtCache.js.map b/dist/src/lib/twtxtCache.js.map new file mode 100644 index 0000000..917b009 --- /dev/null +++ b/dist/src/lib/twtxtCache.js.map @@ -0,0 +1 @@ +{"version":3,"file":"twtxtCache.js","sourceRoot":"","sources":["../../../src/lib/twtxtCache.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,kBAAkB,CAAC;AACnC,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClD,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAGvC;;;;GAIG;AACH,MAAM,CAAC,OAAO,UAAU,UAAU,CAAC,EAClC,eAAe,EACf,aAAa,GACmD;IAChE,IAAI,QAAQ,GAAG,KAAK,CAAC;IAErB,MAAM,KAAK,GAAG,KAAK,CAAC,mBAAmB,CAAC,CAAC;IAEzC,MAAM,KAAK,GAAG,IAAI,SAAS,EAAE,CAAC;IAE9B,MAAM,WAAW,GAAG,KAAK,IAAI,EAAE;QAC9B,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,QAAQ,CAClC,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,aAAa,CAAC,EACzC,MAAM,CACN,CAAC;QAEF,MAAM,UAAU,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;QACxC,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE;YACvC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,UAAU,CAAC,GAA8B,CAAC,CAAC,CAAC,CAAC,aAAa;QAC1E,CAAC,CAAC,CAAC;QAEH,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QAC9B,KAAK,CAAC,SAAS,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;QAE7C,QAAQ,GAAG,IAAI,CAAC;IACjB,CAAC,CAAC;IAEF,WAAW,EAAE,CAAC;IAEd,OAAO;QACN,KAAK;QACL,WAAW;KACX,CAAC;AACH,CAAC"} \ No newline at end of file diff --git a/dist/src/lib/userDB.d.ts b/dist/src/lib/userDB.d.ts new file mode 100644 index 0000000..97ca49a --- /dev/null +++ b/dist/src/lib/userDB.d.ts @@ -0,0 +1,12 @@ +export interface UserDB { + get: (key?: string) => string; + getObject: () => Record; + remove: (key?: string) => void; + set: (key?: string, value?: string) => string; +} +/** + * + * @param directory + * @returns + */ +export default function userDB(directory: string): Promise; diff --git a/dist/src/lib/userDB.js b/dist/src/lib/userDB.js new file mode 100644 index 0000000..d31b443 --- /dev/null +++ b/dist/src/lib/userDB.js @@ -0,0 +1,10 @@ +import simpleDB from './simpleDB.js'; +/** + * + * @param directory + * @returns + */ +export default function userDB(directory) { + return simpleDB('user', directory); +} +//# sourceMappingURL=userDB.js.map \ No newline at end of file diff --git a/dist/src/lib/userDB.js.map b/dist/src/lib/userDB.js.map new file mode 100644 index 0000000..6940ab8 --- /dev/null +++ b/dist/src/lib/userDB.js.map @@ -0,0 +1 @@ +{"version":3,"file":"userDB.js","sourceRoot":"","sources":["../../../src/lib/userDB.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,eAAe,CAAC;AASrC;;;;GAIG;AACH,MAAM,CAAC,OAAO,UAAU,MAAM,CAAC,SAAiB;IAC/C,OAAO,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAoB,CAAC;AACvD,CAAC"} \ No newline at end of file diff --git a/dist/src/lib/utils.d.ts b/dist/src/lib/utils.d.ts new file mode 100644 index 0000000..e2d8814 --- /dev/null +++ b/dist/src/lib/utils.d.ts @@ -0,0 +1,45 @@ +/** + * + * @param userId + * @param secret + * @returns + */ +export declare const generateAccessToken: (userId: string, secret?: string) => string; +/** + * + * @param val + * @returns + */ +export declare const generateEtag: (val: string) => string; +/** + * + * @param userId + * @param secret + * @param extendRefresh + * @returns + */ +export declare const generateRefreshToken: (userId: string, secret?: string, extendRefresh?: boolean) => string; +/** + * + * @param value + * @returns + */ +export declare const getQueryParameterArray: (value?: unknown | unknown[]) => string[]; +/** + * + * @param value + * @returns + */ +export declare const getValueOrFirstEntry: (value: string | string[]) => string | string[]; +/** + * + * @param filePath + * @returns + */ +export declare const loadObjectFromJson: (filePath: string) => Promise; +/** + * + * @param contents + * @param filePath + */ +export declare const saveToJson: (contents: object | string, filePath: string) => Promise; diff --git a/dist/src/lib/utils.js b/dist/src/lib/utils.js new file mode 100644 index 0000000..6d5e7b3 --- /dev/null +++ b/dist/src/lib/utils.js @@ -0,0 +1,67 @@ +import crypto from 'node:crypto'; +import { readFile, writeFile } from 'node:fs/promises'; +import jwt from 'jsonwebtoken'; +import { v4 as uuidv4 } from 'uuid'; +/** + * + * @param userId + * @param secret + * @returns + */ +export const generateAccessToken = (userId, secret = '') => jwt.sign({ id: userId }, secret, { expiresIn: '10m' }); +/** + * + * @param val + * @returns + */ +export const generateEtag = (val) => crypto.createHash('sha256').update(val).digest('hex'); +/** + * + * @param userId + * @param secret + * @param extendRefresh + * @returns + */ +export const generateRefreshToken = (userId, secret = '', extendRefresh = false) => { + const tokenId = uuidv4(); // unique ID for the refresh token + const token = jwt.sign({ id: userId, tokenId }, secret, { + expiresIn: extendRefresh ? '7d' : '1h', + }); + return token; +}; +/** + * + * @param value + * @returns + */ +export const getQueryParameterArray = (value = []) => Array.isArray(value) + ? value.map((val) => `${val}`.trim()) + : [`${value}`.trim()]; +/** + * + * @param value + * @returns + */ +export const getValueOrFirstEntry = (value) => Array.isArray(value) && value.length ? value[0] : value; +/** + * + * @param filePath + * @returns + */ +export const loadObjectFromJson = async (filePath) => { + const contents = await readFile(filePath, { encoding: 'utf8' }); + return JSON.parse(contents); +}; +/** + * + * @param contents + * @param filePath + */ +export const saveToJson = async (contents, filePath) => { + const stringContents = typeof contents === 'string' ? contents : JSON.stringify(contents, null, 2); + await writeFile(filePath, stringContents, { + encoding: 'utf8', + flag: 'w', + }); +}; +//# sourceMappingURL=utils.js.map \ No newline at end of file diff --git a/dist/src/lib/utils.js.map b/dist/src/lib/utils.js.map new file mode 100644 index 0000000..86582bc --- /dev/null +++ b/dist/src/lib/utils.js.map @@ -0,0 +1 @@ +{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../../src/lib/utils.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,GAAG,MAAM,cAAc,CAAC;AAC/B,OAAO,EAAE,EAAE,IAAI,MAAM,EAAE,MAAM,MAAM,CAAC;AAEpC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,MAAc,EAAE,MAAM,GAAG,EAAE,EAAE,EAAE,CAClE,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC;AAExD;;;;GAIG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,GAAW,EAAE,EAAE,CAC3C,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAEvD;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CACnC,MAAc,EACd,MAAM,GAAG,EAAE,EACX,aAAa,GAAG,KAAK,EACpB,EAAE;IACH,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,CAAC,kCAAkC;IAE5D,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE;QACvD,SAAS,EAAE,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI;KACtC,CAAC,CAAC;IAEH,OAAO,KAAK,CAAC;AACd,CAAC,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,QAA6B,EAAE,EAAE,EAAE,CACzE,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;IACnB,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,GAAG,EAAE,CAAC,IAAI,EAAE,CAAC;IACrC,CAAC,CAAC,CAAC,GAAG,KAAK,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC;AAExB;;;;GAIG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,KAAwB,EAAE,EAAE,CAChE,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;AAEzD;;;;GAIG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,KAAK,EAAE,QAAgB,EAAE,EAAE;IAC5D,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,QAAQ,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;IAChE,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;AAC7B,CAAC,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,KAAK,EAC9B,QAAyB,EACzB,QAAgB,EACf,EAAE;IACH,MAAM,cAAc,GACnB,OAAO,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IAE7E,MAAM,SAAS,CAAC,QAAQ,EAAE,cAAc,EAAE;QACzC,QAAQ,EAAE,MAAM;QAChB,IAAI,EAAE,GAAG;KACT,CAAC,CAAC;AACJ,CAAC,CAAC"} \ No newline at end of file diff --git a/dist/src/middlewares/authCheckJWT.d.ts b/dist/src/middlewares/authCheckJWT.d.ts new file mode 100644 index 0000000..06023e7 --- /dev/null +++ b/dist/src/middlewares/authCheckJWT.d.ts @@ -0,0 +1,9 @@ +import type { Request } from 'express'; +import { TwtKprConfiguration } from '../types.js'; +/** + * Checks for a valid JWT, and returns a boolean indicating the result + * + * @param req + * @returns + */ +export default function authCheckJWT(req: Request, config: TwtKprConfiguration): Promise; diff --git a/dist/src/middlewares/authCheckJWT.js b/dist/src/middlewares/authCheckJWT.js new file mode 100644 index 0000000..bf132e3 --- /dev/null +++ b/dist/src/middlewares/authCheckJWT.js @@ -0,0 +1,32 @@ +import Debug from 'debug'; +import jwt from 'jsonwebtoken'; +const debug = Debug('twtkpr:authCheckJWT'); +/** + * Checks for a valid JWT, and returns a boolean indicating the result + * + * @param req + * @returns + */ +export default async function authCheckJWT(req, config) { + debug('beginning'); + const token = req.header('Authorization')?.split(' ')[1]; + if (!token) { + debug('no token'); + return false; + } + debug('token present'); + try { + const decoded = jwt.verify(token, config.accessSecret); + debug({ decoded }); + if (!decoded.id) + return false; + req.username = decoded.id; + } + catch { + debug('invalid token'); + return false; + } + debug('token good'); + return true; +} +//# sourceMappingURL=authCheckJWT.js.map \ No newline at end of file diff --git a/dist/src/middlewares/authCheckJWT.js.map b/dist/src/middlewares/authCheckJWT.js.map new file mode 100644 index 0000000..c83f28b --- /dev/null +++ b/dist/src/middlewares/authCheckJWT.js.map @@ -0,0 +1 @@ +{"version":3,"file":"authCheckJWT.js","sourceRoot":"","sources":["../../../src/middlewares/authCheckJWT.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,GAAG,MAAM,cAAc,CAAC;AAI/B,MAAM,KAAK,GAAG,KAAK,CAAC,qBAAqB,CAAC,CAAC;AAE3C;;;;;GAKG;AACH,MAAM,CAAC,OAAO,CAAC,KAAK,UAAU,YAAY,CACzC,GAAY,EACZ,MAA2B;IAE3B,KAAK,CAAC,WAAW,CAAC,CAAC;IAEnB,MAAM,KAAK,GAAG,GAAG,CAAC,MAAM,CAAC,eAAe,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IACzD,IAAI,CAAC,KAAK,EAAE,CAAC;QACZ,KAAK,CAAC,UAAU,CAAC,CAAC;QAClB,OAAO,KAAK,CAAC;IACd,CAAC;IAED,KAAK,CAAC,eAAe,CAAC,CAAC;IAEvB,IAAI,CAAC;QACJ,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,YAAY,CAAmB,CAAC;QACzE,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC;QAEnB,IAAI,CAAC,OAAO,CAAC,EAAE;YAAE,OAAO,KAAK,CAAC;QAC9B,GAAG,CAAC,QAAQ,GAAG,OAAO,CAAC,EAAE,CAAC;IAC3B,CAAC;IAAC,MAAM,CAAC;QACR,KAAK,CAAC,eAAe,CAAC,CAAC;QACvB,OAAO,KAAK,CAAC;IACd,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,CAAC;IACpB,OAAO,IAAI,CAAC;AACb,CAAC"} \ No newline at end of file diff --git a/dist/src/middlewares/csrfProtection.d.ts b/dist/src/middlewares/csrfProtection.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/src/middlewares/csrfProtection.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/src/middlewares/csrfProtection.js b/dist/src/middlewares/csrfProtection.js new file mode 100644 index 0000000..8357237 --- /dev/null +++ b/dist/src/middlewares/csrfProtection.js @@ -0,0 +1,27 @@ +export {}; +/* +import { doubleCsrf } from "csrf-csrf"; + +const { + invalidCsrfTokenError, // This is just for convenience if you plan on making your own middleware. + generateCsrfToken, // Use this in your routes to provide a CSRF token. + validateRequest, // Also a convenience if you plan on making your own middleware. + doubleCsrfProtection, // This is the default CSRF protection middleware. +} = doubleCsrf({ + getSecret: (req) => 'return some cryptographically pseudorandom secret here', + getSessionIdentifier: (req) => req.session.id // return the requests unique identifier +}); + + +const csrfTokenRoute = (req, res) => { + const csrfToken = generateCsrfToken(req, res); + // You could also pass the token into the context of a HTML response. + res.json({ csrfToken }); +}; + +export { + csrfTokenRoute, + doubleCsrfProtection, +} +*/ +//# sourceMappingURL=csrfProtection.js.map \ No newline at end of file diff --git a/dist/src/middlewares/csrfProtection.js.map b/dist/src/middlewares/csrfProtection.js.map new file mode 100644 index 0000000..c451d93 --- /dev/null +++ b/dist/src/middlewares/csrfProtection.js.map @@ -0,0 +1 @@ +{"version":3,"file":"csrfProtection.js","sourceRoot":"","sources":["../../../src/middlewares/csrfProtection.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;EAwBE"} \ No newline at end of file diff --git a/dist/src/middlewares/index.d.ts b/dist/src/middlewares/index.d.ts new file mode 100644 index 0000000..0d9fe81 --- /dev/null +++ b/dist/src/middlewares/index.d.ts @@ -0,0 +1,4 @@ +export { default as authCheck } from './authCheckJWT.js'; +export { default as memoryCache } from './postHandler/memoryCache.js'; +export { default as postHandler } from './postHandler/index.js'; +export { default as queryHandler } from './queryHandler/index.js'; diff --git a/dist/src/middlewares/index.js b/dist/src/middlewares/index.js new file mode 100644 index 0000000..f7462c7 --- /dev/null +++ b/dist/src/middlewares/index.js @@ -0,0 +1,5 @@ +export { default as authCheck } from './authCheckJWT.js'; +export { default as memoryCache } from './postHandler/memoryCache.js'; +export { default as postHandler } from './postHandler/index.js'; +export { default as queryHandler } from './queryHandler/index.js'; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/dist/src/middlewares/index.js.map b/dist/src/middlewares/index.js.map new file mode 100644 index 0000000..e2366e4 --- /dev/null +++ b/dist/src/middlewares/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/middlewares/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,8BAA8B,CAAC;AACtE,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAChE,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,yBAAyB,CAAC"} \ No newline at end of file diff --git a/dist/src/middlewares/postHandler/index.d.ts b/dist/src/middlewares/postHandler/index.d.ts new file mode 100644 index 0000000..401cc49 --- /dev/null +++ b/dist/src/middlewares/postHandler/index.d.ts @@ -0,0 +1 @@ +export { default } from "./postHandler.js"; diff --git a/dist/src/middlewares/postHandler/index.js b/dist/src/middlewares/postHandler/index.js new file mode 100644 index 0000000..b29a439 --- /dev/null +++ b/dist/src/middlewares/postHandler/index.js @@ -0,0 +1,2 @@ +export { default } from "./postHandler.js"; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/dist/src/middlewares/postHandler/index.js.map b/dist/src/middlewares/postHandler/index.js.map new file mode 100644 index 0000000..4df61a0 --- /dev/null +++ b/dist/src/middlewares/postHandler/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/middlewares/postHandler/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC"} \ No newline at end of file diff --git a/dist/src/middlewares/postHandler/login.d.ts b/dist/src/middlewares/postHandler/login.d.ts new file mode 100644 index 0000000..d0e06f9 --- /dev/null +++ b/dist/src/middlewares/postHandler/login.d.ts @@ -0,0 +1,10 @@ +import type { Request, Response } from 'express'; +import { TwtKprConfiguration } from '../../types.js'; +/** + * Handles login request and (if successful) returns the JWT access token wile setting the refresh n the + * + * @param req + * @param res + * @returns + */ +export default function loginHandler(req: Request, res: Response, config: TwtKprConfiguration): Promise; diff --git a/dist/src/middlewares/postHandler/login.js b/dist/src/middlewares/postHandler/login.js new file mode 100644 index 0000000..aac0755 --- /dev/null +++ b/dist/src/middlewares/postHandler/login.js @@ -0,0 +1,67 @@ +import bcrypt from 'bcryptjs'; +import Debug from 'debug'; +import { env } from '../../lib/env.js'; +import refreshTokensDB from '../../lib/refreshTokensDB.js'; +import userDB from '../../lib/userDB.js'; +import { generateAccessToken, generateEtag, generateRefreshToken, } from '../../lib/utils.js'; +const debug = Debug('twtkpr:login'); +/** + * Handles login request and (if successful) returns the JWT access token wile setting the refresh n the + * + * @param req + * @param res + * @returns + */ +export default async function loginHandler(req, res, config) { + const { accessSecret, privateDirectory, refreshSecret } = config; + debug('starting'); + try { + const tokens = await refreshTokensDB(privateDirectory); + const users = await userDB(privateDirectory); + const { username, password, rememberToggle } = req.body; + if (!username || !password || !users.get(username)) { + debug('no values found', username); + res.status(401).end(); + return; + } + const isMatch = await bcrypt.compare(password, users.get(username)); + if (!isMatch) { + privateDirectory; + debug('no match'); + res.status(401).end(); + return; + } + debug('generating tokens'); + const accessToken = generateAccessToken(username, accessSecret); + debug(`access token: ${accessToken}`); + const refreshToken = generateRefreshToken(username, refreshSecret, !!rememberToggle); + debug(`refresh token: ${refreshToken}`); + debug('setting tokens'); + tokens.set(username, (tokens.get(username) || []).concat([refreshToken])); + debug('setting refreshToken cookie'); + res.cookie('refreshToken', refreshToken, { + httpOnly: true, + secure: env.NODE_ENV === 'production', + sameSite: 'strict', + // 1 hour or 7 days + maxAge: (rememberToggle ? 1 : 7 * 24) * 60 * 60 * 1000, + }); + if (rememberToggle) { + debug('setting accessToken cookie'); + /* + res.cookie('accessToken', accessToken, { + httpOnly: false, + secure: env.NODE_ENV === 'production', + sameSite: 'strict', + }); + */ + } + debug('setting response'); + res.set('etag', generateEtag(accessToken)).status(200).send(accessToken); + } + catch (err) { + console.error(err); + res.status(500).end(); + } +} +//# sourceMappingURL=login.js.map \ No newline at end of file diff --git a/dist/src/middlewares/postHandler/login.js.map b/dist/src/middlewares/postHandler/login.js.map new file mode 100644 index 0000000..aed1c26 --- /dev/null +++ b/dist/src/middlewares/postHandler/login.js.map @@ -0,0 +1 @@ +{"version":3,"file":"login.js","sourceRoot":"","sources":["../../../../src/middlewares/postHandler/login.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/src/middlewares/postHandler/logout.d.ts b/dist/src/middlewares/postHandler/logout.d.ts new file mode 100644 index 0000000..5126254 --- /dev/null +++ b/dist/src/middlewares/postHandler/logout.d.ts @@ -0,0 +1,10 @@ +import type { Request, Response } from 'express'; +import { TwtKprConfiguration } from '../../types.js'; +/** + * Handles logout request and clears the token cookies + * + * @param req + * @param res + * @returns + */ +export default function logoutHandler(req: Request, res: Response, config: TwtKprConfiguration): Promise; diff --git a/dist/src/middlewares/postHandler/logout.js b/dist/src/middlewares/postHandler/logout.js new file mode 100644 index 0000000..e84dcef --- /dev/null +++ b/dist/src/middlewares/postHandler/logout.js @@ -0,0 +1,20 @@ +import Debug from 'debug'; +const debug = Debug('twtkpr:logout'); +/** + * Handles logout request and clears the token cookies + * + * @param req + * @param res + * @returns + */ +export default async function logoutHandler(req, res, config) { + const { mainRoute } = config; + debug('logging out'); + res + .status(200) + .clearCookie('refreshToken') + .clearCookie('accessToken') + .redirect(mainRoute); + return; +} +//# sourceMappingURL=logout.js.map \ No newline at end of file diff --git a/dist/src/middlewares/postHandler/logout.js.map b/dist/src/middlewares/postHandler/logout.js.map new file mode 100644 index 0000000..5206512 --- /dev/null +++ b/dist/src/middlewares/postHandler/logout.js.map @@ -0,0 +1 @@ +{"version":3,"file":"logout.js","sourceRoot":"","sources":["../../../../src/middlewares/postHandler/logout.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAC;AAI1B,MAAM,KAAK,GAAG,KAAK,CAAC,eAAe,CAAC,CAAC;AAErC;;;;;;GAMG;AACH,MAAM,CAAC,OAAO,CAAC,KAAK,UAAU,aAAa,CAC1C,GAAY,EACZ,GAAa,EACb,MAA2B;IAE3B,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,CAAC;IAC7B,KAAK,CAAC,aAAa,CAAC,CAAC;IAErB,GAAG;SACD,MAAM,CAAC,GAAG,CAAC;SACX,WAAW,CAAC,cAAc,CAAC;SAC3B,WAAW,CAAC,aAAa,CAAC;SAC1B,QAAQ,CAAC,SAAS,CAAC,CAAC;IAEtB,OAAO;AACR,CAAC"} \ No newline at end of file diff --git a/dist/src/middlewares/postHandler/memoryCache.d.ts b/dist/src/middlewares/postHandler/memoryCache.d.ts new file mode 100644 index 0000000..86edc7c --- /dev/null +++ b/dist/src/middlewares/postHandler/memoryCache.d.ts @@ -0,0 +1,12 @@ +import type { NextFunction, Request, Response } from 'express'; +import NodeCache from '@cacheable/node-cache'; +/** + * + * @param req + * @param res + * @param next + * @param cache + * @param reloadCache + * @returns + */ +export default function memoryCache(req: Request, res: Response, next: NextFunction, cache: NodeCache, reloadCache: () => Promise): Promise; diff --git a/dist/src/middlewares/postHandler/memoryCache.js b/dist/src/middlewares/postHandler/memoryCache.js new file mode 100644 index 0000000..a8ccafc --- /dev/null +++ b/dist/src/middlewares/postHandler/memoryCache.js @@ -0,0 +1,25 @@ +import Debug from 'debug'; +const debug = Debug('twtkpr:memoryCache'); +/** + * + * @param req + * @param res + * @param next + * @param cache + * @param reloadCache + * @returns + */ +export default async function memoryCache(req, res, next, cache, reloadCache) { + if (cache.keys().length && !['DELETE', 'POST', 'PUT'].includes(req.method)) { + next(); + return; + } + reloadCache() + .then(() => { + next(); + }) + .catch((err) => { + console.error(err); + }); +} +//# sourceMappingURL=memoryCache.js.map \ No newline at end of file diff --git a/dist/src/middlewares/postHandler/memoryCache.js.map b/dist/src/middlewares/postHandler/memoryCache.js.map new file mode 100644 index 0000000..82373dc --- /dev/null +++ b/dist/src/middlewares/postHandler/memoryCache.js.map @@ -0,0 +1 @@ +{"version":3,"file":"memoryCache.js","sourceRoot":"","sources":["../../../../src/middlewares/postHandler/memoryCache.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,MAAM,KAAK,GAAG,KAAK,CAAC,oBAAoB,CAAC,CAAC;AAE1C;;;;;;;;GAQG;AACH,MAAM,CAAC,OAAO,CAAC,KAAK,UAAU,WAAW,CACxC,GAAY,EACZ,GAAa,EACb,IAAkB,EAClB,KAAyB,EACzB,WAAgC;IAEhC,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,IAAI,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;QAC5E,IAAI,EAAE,CAAC;QACP,OAAO;IACR,CAAC;IAED,WAAW,EAAE;SACX,IAAI,CAAC,GAAG,EAAE;QACV,IAAI,EAAE,CAAC;IACR,CAAC,CAAC;SACD,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;QACd,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACpB,CAAC,CAAC,CAAC;AACL,CAAC"} \ No newline at end of file diff --git a/dist/src/middlewares/postHandler/postHandler.d.ts b/dist/src/middlewares/postHandler/postHandler.d.ts new file mode 100644 index 0000000..9517e5a --- /dev/null +++ b/dist/src/middlewares/postHandler/postHandler.d.ts @@ -0,0 +1,7 @@ +import { TwtKprConfiguration } from '../../types.js'; +/** + * + * @param config + * @returns + */ +export default function postHandler(config: TwtKprConfiguration): import("express-serve-static-core").Router; diff --git a/dist/src/middlewares/postHandler/postHandler.js b/dist/src/middlewares/postHandler/postHandler.js new file mode 100644 index 0000000..d2b570d --- /dev/null +++ b/dist/src/middlewares/postHandler/postHandler.js @@ -0,0 +1,60 @@ +import Debug from 'debug'; +import express from 'express'; +import rateLimit from 'express-rate-limit'; +import authCheck from '../../middlewares/authCheckJWT.js'; +import login from './login.js'; +import logout from './logout.js'; +import refresh from './refresh.js'; +import twt from './twt.js'; +import editFile from '../putHandler/editFile.js'; +const debug = Debug('twtkpr:postHandler'); +/** + * + * @param config + * @returns + */ +export default function postHandler(config) { + const { postLimiterConfiguration } = config; + const { active: isLimiterActive, ...otherLimiterProps } = postLimiterConfiguration ?? {}; + const postLimiter = isLimiterActive + ? rateLimit({ + ...otherLimiterProps, + }) + : (req, res, next) => { + next(); + }; + const { mainRoute } = config; + const router = express.Router(); + router.post('/', postLimiter, async (req, res, next) => { + const { content, type } = req.body ?? {}; + debug('post', { type, path: req.path }); + if (type === 'logout') { + debug('logging out'); + res.clearCookie('refreshToken'); + res.clearCookie('accessToken'); + res.redirect(mainRoute); + return; + } + if (type === 'login') + return login(req, res, config); + if (type === 'logout') + return logout(req, res, config); + if (type === 'refresh') + return refresh(req, res, config); + debug('checking auth'); + const isLoggedIn = await authCheck(req, config); + if (!isLoggedIn) { + debug('auth check failed'); + next(); + return; + } + debug('auth check succeeded'); + if (type === 'twt' || content) + return twt(req, res, config); + if (type === 'editFile') + return editFile(req, res, config); + next(); + }); + return router; +} +//# sourceMappingURL=postHandler.js.map \ No newline at end of file diff --git a/dist/src/middlewares/postHandler/postHandler.js.map b/dist/src/middlewares/postHandler/postHandler.js.map new file mode 100644 index 0000000..a13f01c --- /dev/null +++ b/dist/src/middlewares/postHandler/postHandler.js.map @@ -0,0 +1 @@ +{"version":3,"file":"postHandler.js","sourceRoot":"","sources":["../../../../src/middlewares/postHandler/postHandler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,OAA4C,MAAM,SAAS,CAAC;AACnE,OAAO,SAAS,MAAM,oBAAoB,CAAC;AAE3C,OAAO,SAAS,MAAM,mCAAmC,CAAC;AAE1D,OAAO,KAAK,MAAM,YAAY,CAAC;AAC/B,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,OAAO,MAAM,cAAc,CAAC;AACnC,OAAO,GAAG,MAAM,UAAU,CAAC;AAC3B,OAAO,QAAQ,MAAM,2BAA2B,CAAC;AAEjD,MAAM,KAAK,GAAG,KAAK,CAAC,oBAAoB,CAAC,CAAC;AAE1C;;;;GAIG;AACH,MAAM,CAAC,OAAO,UAAU,WAAW,CAAC,MAA2B;IAC9D,MAAM,EAAE,wBAAwB,EAAE,GAAG,MAAM,CAAC;IAC5C,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,GAAG,iBAAiB,EAAE,GACtD,wBAAwB,IAAI,EAAE,CAAC;IAEhC,MAAM,WAAW,GAAG,eAAe;QAClC,CAAC,CAAC,SAAS,CAAC;YACV,GAAG,iBAAiB;SACpB,CAAC;QACH,CAAC,CAAC,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;YACpD,IAAI,EAAE,CAAC;QACR,CAAC,CAAC;IAEJ,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,CAAC;IAE7B,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAEhC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,WAAW,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QACtD,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC;QACzC,KAAK,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;QAExC,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;YACvB,KAAK,CAAC,aAAa,CAAC,CAAC;YACrB,GAAG,CAAC,WAAW,CAAC,cAAc,CAAC,CAAC;YAChC,GAAG,CAAC,WAAW,CAAC,aAAa,CAAC,CAAC;YAC/B,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;YACxB,OAAO;QACR,CAAC;QAED,IAAI,IAAI,KAAK,OAAO;YAAE,OAAO,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;QACrD,IAAI,IAAI,KAAK,QAAQ;YAAE,OAAO,MAAM,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;QACvD,IAAI,IAAI,KAAK,SAAS;YAAE,OAAO,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;QAEzD,KAAK,CAAC,eAAe,CAAC,CAAC;QACvB,MAAM,UAAU,GAAG,MAAM,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QAChD,IAAI,CAAC,UAAU,EAAE,CAAC;YACjB,KAAK,CAAC,mBAAmB,CAAC,CAAC;YAC3B,IAAI,EAAE,CAAC;YACP,OAAO;QACR,CAAC;QACD,KAAK,CAAC,sBAAsB,CAAC,CAAC;QAE9B,IAAI,IAAI,KAAK,KAAK,IAAI,OAAO;YAAE,OAAO,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;QAC5D,IAAI,IAAI,KAAK,UAAU;YAAE,OAAO,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;QAE3D,IAAI,EAAE,CAAC;IACR,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AACf,CAAC"} \ No newline at end of file diff --git a/dist/src/middlewares/postHandler/refresh.d.ts b/dist/src/middlewares/postHandler/refresh.d.ts new file mode 100644 index 0000000..d7292c8 --- /dev/null +++ b/dist/src/middlewares/postHandler/refresh.d.ts @@ -0,0 +1,9 @@ +import type { Request, Response } from 'express'; +import { TwtKprConfiguration } from '../../types.js'; +/** + * Issues a new JWT and updates the refresh token in the cookie + * + * @param req + * @param res + */ +export default function refresh(req: Request, res: Response, config: TwtKprConfiguration): Promise; diff --git a/dist/src/middlewares/postHandler/refresh.js b/dist/src/middlewares/postHandler/refresh.js new file mode 100644 index 0000000..fd2ad7a --- /dev/null +++ b/dist/src/middlewares/postHandler/refresh.js @@ -0,0 +1,78 @@ +import Debug from 'debug'; +import jwt from 'jsonwebtoken'; +import { env } from '../../lib/env.js'; +import refreshTokensDB from '../../lib/refreshTokensDB.js'; +import { generateAccessToken, generateEtag, generateRefreshToken, } from '../../lib/utils.js'; +const debug = Debug('twtkpr:refresh'); +/** + * Issues a new JWT and updates the refresh token in the cookie + * + * @param req + * @param res + */ +export default async function refresh(req, res, config) { + const send401 = (message) => { + debug(message); + res + .clearCookie('accessToken') + .clearCookie('refreshToken') + .status(401) + .send(message ?? 'Unauthorized'); + return; + }; + try { + const tokens = await refreshTokensDB(config.privateDirectory); + const oldToken = req.cookies.refreshToken; + debug(oldToken); + if (!oldToken) + return send401('Unauthorized'); + let decoded = { id: '' }; + try { + decoded = jwt.verify(oldToken, config.refreshSecret); + debug({ decoded }); + } + catch (err) { + return send401('Refresh token invalid'); + } + const username = req.username ?? decoded.id; + if (!username) + return send401('Missing username'); + const currentTime = Math.floor(Date.now() / 1000); + // cleanup tokens on load + const validTokens = (tokens.get(decoded.id) ?? []).filter((token) => { + const val = jwt.decode(token); + return val && (val.exp ?? 0) >= currentTime; + }); + // If token is invalid or not the latest one + if (!validTokens.includes(oldToken)) { + debug('token missing from list'); + return send401('Invalid refresh token'); + } + debug('generating new tokens'); + const newAccessToken = generateAccessToken(req.username || decoded.id, config.accessSecret); + const newRefreshToken = generateRefreshToken(req.username || decoded.id, config.refreshSecret); + debug('updating token list'); + tokens.set(req.username || decoded.id, validTokens + .filter((token) => token !== oldToken) + .concat([newRefreshToken])); + debug('setting httpOnly cookie with new refresh token'); + res.cookie('refreshToken', newRefreshToken, { + httpOnly: true, + secure: env.NODE_ENV === 'production', + sameSite: 'strict', + // 1 hour or 7 days + maxAge: (!!req.query.rememberToggle ? 1 : 7 * 24) * 60 * 60 * 1000, + }); + // Return the new access token in body + debug('generating response'); + res + .set('etag', generateEtag(newAccessToken)) + .status(200) + .send(newAccessToken); + } + catch (err) { + console.error(err); + res.status(500).end(); + } +} +//# sourceMappingURL=refresh.js.map \ No newline at end of file diff --git a/dist/src/middlewares/postHandler/refresh.js.map b/dist/src/middlewares/postHandler/refresh.js.map new file mode 100644 index 0000000..ae21e4f --- /dev/null +++ b/dist/src/middlewares/postHandler/refresh.js.map @@ -0,0 +1 @@ +{"version":3,"file":"refresh.js","sourceRoot":"","sources":["../../../../src/middlewares/postHandler/refresh.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/src/middlewares/postHandler/twt.d.ts b/dist/src/middlewares/postHandler/twt.d.ts new file mode 100644 index 0000000..ad7e342 --- /dev/null +++ b/dist/src/middlewares/postHandler/twt.d.ts @@ -0,0 +1,9 @@ +import type { Request, Response } from 'express'; +import { TwtKprConfiguration } from '../../types.js'; +/** + * Creates a new twt, appending it to the bottom of the TWTXT file + * + * @param req + * @param res + */ +export default function twt(req: Request, res: Response, config: TwtKprConfiguration): void; diff --git a/dist/src/middlewares/postHandler/twt.js b/dist/src/middlewares/postHandler/twt.js new file mode 100644 index 0000000..b51bd50 --- /dev/null +++ b/dist/src/middlewares/postHandler/twt.js @@ -0,0 +1,21 @@ +import dayjs from 'dayjs'; +import fs from 'node:fs'; +import { join } from 'node:path'; +/** + * Creates a new twt, appending it to the bottom of the TWTXT file + * + * @param req + * @param res + */ +export default function twt(req, res, config) { + const { content } = req.body ?? {}; + const date = dayjs().format(); + const twt = `${date}\t${content.trim()}\n`; + const stream = fs.createWriteStream(join(config.publicDirectory, config.twtxtFilename), { + flags: 'a', + }); + stream.write(twt); + stream.end(); + res.status(200).send(twt); +} +//# sourceMappingURL=twt.js.map \ No newline at end of file diff --git a/dist/src/middlewares/postHandler/twt.js.map b/dist/src/middlewares/postHandler/twt.js.map new file mode 100644 index 0000000..22031d4 --- /dev/null +++ b/dist/src/middlewares/postHandler/twt.js.map @@ -0,0 +1 @@ +{"version":3,"file":"twt.js","sourceRoot":"","sources":["../../../../src/middlewares/postHandler/twt.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAIjC;;;;;GAKG;AACH,MAAM,CAAC,OAAO,UAAU,GAAG,CAC1B,GAAY,EACZ,GAAa,EACb,MAA2B;IAE3B,MAAM,EAAE,OAAO,EAAE,GAAG,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC;IAEnC,MAAM,IAAI,GAAG,KAAK,EAAE,CAAC,MAAM,EAAE,CAAC;IAC9B,MAAM,GAAG,GAAG,GAAG,IAAI,KAAK,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC;IAE3C,MAAM,MAAM,GAAG,EAAE,CAAC,iBAAiB,CAClC,IAAI,CAAC,MAAM,CAAC,eAAe,EAAE,MAAM,CAAC,aAAa,CAAC,EAClD;QACC,KAAK,EAAE,GAAG;KACV,CACD,CAAC;IAEF,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAClB,MAAM,CAAC,GAAG,EAAE,CAAC;IAEb,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC3B,CAAC"} \ No newline at end of file diff --git a/dist/src/middlewares/putHandler/editFile.d.ts b/dist/src/middlewares/putHandler/editFile.d.ts new file mode 100644 index 0000000..c68224b --- /dev/null +++ b/dist/src/middlewares/putHandler/editFile.d.ts @@ -0,0 +1,9 @@ +import type { Request, Response } from "express"; +import { TwtKprConfiguration } from "../../types.js"; +/** + * Creates a new twt, appending it to the bottom of the TWTXT file + * + * @param req + * @param res + */ +export default function editFile(req: Request, res: Response, config: TwtKprConfiguration): void; diff --git a/dist/src/middlewares/putHandler/editFile.js b/dist/src/middlewares/putHandler/editFile.js new file mode 100644 index 0000000..39480c4 --- /dev/null +++ b/dist/src/middlewares/putHandler/editFile.js @@ -0,0 +1,23 @@ +import fs from "node:fs"; +import path from "node:path"; +/** + * Creates a new twt, appending it to the bottom of the TWTXT file + * + * @param req + * @param res + */ +export default function editFile(req, res, config) { + const { fileContents } = req.body ?? {}; + if (!fileContents) { + res.status(400).send("Missing fileContents"); + return; + } + const stream = fs.createWriteStream(path.join(config.publicDirectory, config.twtxtFilename), { + flags: "w", + start: 0, + }); + stream.write(fileContents); + stream.end(); + res.type("text").status(200).send(fileContents); +} +//# sourceMappingURL=editFile.js.map \ No newline at end of file diff --git a/dist/src/middlewares/putHandler/editFile.js.map b/dist/src/middlewares/putHandler/editFile.js.map new file mode 100644 index 0000000..a603c9d --- /dev/null +++ b/dist/src/middlewares/putHandler/editFile.js.map @@ -0,0 +1 @@ +{"version":3,"file":"editFile.js","sourceRoot":"","sources":["../../../../src/middlewares/putHandler/editFile.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAI7B;;;;;GAKG;AACH,MAAM,CAAC,OAAO,UAAU,QAAQ,CAC/B,GAAY,EACZ,GAAa,EACb,MAA2B;IAE3B,MAAM,EAAE,YAAY,EAAE,GAAG,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC;IAExC,IAAI,CAAC,YAAY,EAAE,CAAC;QACnB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAC;QAC7C,OAAO;IACR,CAAC;IAED,MAAM,MAAM,GAAG,EAAE,CAAC,iBAAiB,CAClC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,eAAe,EAAE,MAAM,CAAC,aAAa,CAAC,EACvD;QACC,KAAK,EAAE,GAAG;QACV,KAAK,EAAE,CAAC;KACR,CACD,CAAC;IAEF,MAAM,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;IAC3B,MAAM,CAAC,GAAG,EAAE,CAAC;IAEb,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;AACjD,CAAC"} \ No newline at end of file diff --git a/dist/src/middlewares/putHandler/index.d.ts b/dist/src/middlewares/putHandler/index.d.ts new file mode 100644 index 0000000..30a6615 --- /dev/null +++ b/dist/src/middlewares/putHandler/index.d.ts @@ -0,0 +1 @@ +export { default } from "./putHandler.js"; diff --git a/dist/src/middlewares/putHandler/index.js b/dist/src/middlewares/putHandler/index.js new file mode 100644 index 0000000..4c74a05 --- /dev/null +++ b/dist/src/middlewares/putHandler/index.js @@ -0,0 +1,2 @@ +export { default } from "./putHandler.js"; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/dist/src/middlewares/putHandler/index.js.map b/dist/src/middlewares/putHandler/index.js.map new file mode 100644 index 0000000..4b6b302 --- /dev/null +++ b/dist/src/middlewares/putHandler/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/middlewares/putHandler/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC"} \ No newline at end of file diff --git a/dist/src/middlewares/putHandler/putHandler.d.ts b/dist/src/middlewares/putHandler/putHandler.d.ts new file mode 100644 index 0000000..4ee6522 --- /dev/null +++ b/dist/src/middlewares/putHandler/putHandler.d.ts @@ -0,0 +1,7 @@ +import { TwtKprConfiguration } from '../../types.js'; +/** + * + * @param config + * @returns + */ +export default function putHandler(config: TwtKprConfiguration): import("express-serve-static-core").Router; diff --git a/dist/src/middlewares/putHandler/putHandler.js b/dist/src/middlewares/putHandler/putHandler.js new file mode 100644 index 0000000..66a37a0 --- /dev/null +++ b/dist/src/middlewares/putHandler/putHandler.js @@ -0,0 +1,26 @@ +import Debug from 'debug'; +import express from 'express'; +import authCheck from '../../middlewares/authCheckJWT.js'; +import editFile from './editFile.js'; +const debug = Debug('twtkpr:putHandler'); +/** + * + * @param config + * @returns + */ +export default function putHandler(config) { + const router = express.Router(); + router.put('/', (req, res, next) => { + debug('put', { path: req.path }); + debug('checking auth'); + if (!authCheck(req, config)) { + debug('auth check failed'); + next(); + return; + } + debug('auth check succeeded'); + return editFile(req, res, config); + }); + return router; +} +//# sourceMappingURL=putHandler.js.map \ No newline at end of file diff --git a/dist/src/middlewares/putHandler/putHandler.js.map b/dist/src/middlewares/putHandler/putHandler.js.map new file mode 100644 index 0000000..1dcbf6b --- /dev/null +++ b/dist/src/middlewares/putHandler/putHandler.js.map @@ -0,0 +1 @@ +{"version":3,"file":"putHandler.js","sourceRoot":"","sources":["../../../../src/middlewares/putHandler/putHandler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,OAAO,MAAM,SAAS,CAAC;AAE9B,OAAO,SAAS,MAAM,mCAAmC,CAAC;AAE1D,OAAO,QAAQ,MAAM,eAAe,CAAC;AAErC,MAAM,KAAK,GAAG,KAAK,CAAC,mBAAmB,CAAC,CAAC;AAEzC;;;;GAIG;AACH,MAAM,CAAC,OAAO,UAAU,UAAU,CAAC,MAA2B;IAC7D,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAEhC,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QAClC,KAAK,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;QAEjC,KAAK,CAAC,eAAe,CAAC,CAAC;QAEvB,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC,EAAE,CAAC;YAC7B,KAAK,CAAC,mBAAmB,CAAC,CAAC;YAC3B,IAAI,EAAE,CAAC;YACP,OAAO;QACR,CAAC;QAED,KAAK,CAAC,sBAAsB,CAAC,CAAC;QAE9B,OAAO,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;IACnC,CAAC,CAAC,CAAC;IACH,OAAO,MAAM,CAAC;AACf,CAAC"} \ No newline at end of file diff --git a/dist/src/middlewares/queryHandler/followingHandler.d.ts b/dist/src/middlewares/queryHandler/followingHandler.d.ts new file mode 100644 index 0000000..7c82529 --- /dev/null +++ b/dist/src/middlewares/queryHandler/followingHandler.d.ts @@ -0,0 +1,11 @@ +import type { Request, Response } from 'express'; +import { QueryParameters } from '../../types.js'; +import NodeCache from '@cacheable/node-cache'; +/** + * + * @param req + * @param res + * @param cache + * @param followingParameter + */ +export default function followingHandler(req: Request, res: Response, cache: NodeCache, followingParameter: QueryParameters['following']): void; diff --git a/dist/src/middlewares/queryHandler/followingHandler.js b/dist/src/middlewares/queryHandler/followingHandler.js new file mode 100644 index 0000000..691c274 --- /dev/null +++ b/dist/src/middlewares/queryHandler/followingHandler.js @@ -0,0 +1,39 @@ +import { generateEtag, getQueryParameterArray, getValueOrFirstEntry, } from '../../lib/utils.js'; +/** + * + * @param req + * @param res + * @param cache + * @param followingParameter + */ +export default function followingHandler(req, res, cache, followingParameter) { + const followingsToMatch = getQueryParameterArray(req.query[followingParameter]); + const nicksToMatch = getQueryParameterArray(req.query.nick); + const urlsToMatch = getQueryParameterArray(req.query.url); + const searchTermsToMatch = [ + ...getQueryParameterArray(req.query.search), + ...getQueryParameterArray(req.query.s), + ]; + const wantsJson = req.is('json') || + getValueOrFirstEntry(getQueryParameterArray(req.query.format)) === 'json'; + if (wantsJson) + res.set('content-type', 'application/json'); + else + res.set('content-type', 'text/plain'); + const matchedFollowing = cache.get('following').filter(({ nick, url }) => (!followingsToMatch.length || + (followingsToMatch.length === 1 && followingsToMatch[0] === '') || + followingsToMatch.includes(nick) || + followingsToMatch.includes(`@${nick}`) || + followingsToMatch.includes(url)) && + (!nicksToMatch.length || + nicksToMatch.includes(nick) || + nicksToMatch.includes(`@${nick}`)) && + (!urlsToMatch.length || urlsToMatch.includes(url)) && + (!searchTermsToMatch.length || + searchTermsToMatch.some((term) => nick.includes(term) || url.includes(term)))); + const result = wantsJson + ? JSON.stringify(matchedFollowing) + : matchedFollowing.map(({ nick, url }) => `@${nick} ${url}`).join('\n'); + res.set('etag', generateEtag(result)).send(result); +} +//# sourceMappingURL=followingHandler.js.map \ No newline at end of file diff --git a/dist/src/middlewares/queryHandler/followingHandler.js.map b/dist/src/middlewares/queryHandler/followingHandler.js.map new file mode 100644 index 0000000..82e0a16 --- /dev/null +++ b/dist/src/middlewares/queryHandler/followingHandler.js.map @@ -0,0 +1 @@ +{"version":3,"file":"followingHandler.js","sourceRoot":"","sources":["../../../../src/middlewares/queryHandler/followingHandler.ts"],"names":[],"mappings":"AAGA,OAAO,EACN,YAAY,EACZ,sBAAsB,EACtB,oBAAoB,GACpB,MAAM,oBAAoB,CAAC;AAI5B;;;;;;GAMG;AACH,MAAM,CAAC,OAAO,UAAU,gBAAgB,CACvC,GAAY,EACZ,GAAa,EACb,KAAyB,EACzB,kBAAgD;IAEhD,MAAM,iBAAiB,GAAG,sBAAsB,CAC/C,GAAG,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAC7B,CAAC;IAEF,MAAM,YAAY,GAAG,sBAAsB,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC5D,MAAM,WAAW,GAAG,sBAAsB,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAE1D,MAAM,kBAAkB,GAAG;QAC1B,GAAG,sBAAsB,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC;QAC3C,GAAG,sBAAsB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC;KACtC,CAAC;IAEF,MAAM,SAAS,GACd,GAAG,CAAC,EAAE,CAAC,MAAM,CAAC;QACd,oBAAoB,CAAC,sBAAsB,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,KAAK,MAAM,CAAC;IAC3E,IAAI,SAAS;QAAE,GAAG,CAAC,GAAG,CAAC,cAAc,EAAE,kBAAkB,CAAC,CAAC;;QACtD,GAAG,CAAC,GAAG,CAAC,cAAc,EAAE,YAAY,CAAC,CAAC;IAE3C,MAAM,gBAAgB,GAAI,KAAK,CAAC,GAAG,CAAC,WAAW,CAAa,CAAC,MAAM,CAClE,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE,CACjB,CAAC,CAAC,iBAAiB,CAAC,MAAM;QACzB,CAAC,iBAAiB,CAAC,MAAM,KAAK,CAAC,IAAI,iBAAiB,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC;QAC/D,iBAAiB,CAAC,QAAQ,CAAC,IAAI,CAAC;QAChC,iBAAiB,CAAC,QAAQ,CAAC,IAAI,IAAI,EAAE,CAAC;QACtC,iBAAiB,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QACjC,CAAC,CAAC,YAAY,CAAC,MAAM;YACpB,YAAY,CAAC,QAAQ,CAAC,IAAI,CAAC;YAC3B,YAAY,CAAC,QAAQ,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;QACnC,CAAC,CAAC,WAAW,CAAC,MAAM,IAAI,WAAW,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QAClD,CAAC,CAAC,kBAAkB,CAAC,MAAM;YAC1B,kBAAkB,CAAC,IAAI,CACtB,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CACnD,CAAC,CACJ,CAAC;IAEF,MAAM,MAAM,GAAG,SAAS;QACvB,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,gBAAgB,CAAC;QAClC,CAAC,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC,IAAI,IAAI,IAAI,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAEzE,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AACpD,CAAC"} \ No newline at end of file diff --git a/dist/src/middlewares/queryHandler/index.d.ts b/dist/src/middlewares/queryHandler/index.d.ts new file mode 100644 index 0000000..9f0a132 --- /dev/null +++ b/dist/src/middlewares/queryHandler/index.d.ts @@ -0,0 +1 @@ +export { default } from "./queryHandler.js"; diff --git a/dist/src/middlewares/queryHandler/index.js b/dist/src/middlewares/queryHandler/index.js new file mode 100644 index 0000000..3a1a776 --- /dev/null +++ b/dist/src/middlewares/queryHandler/index.js @@ -0,0 +1,2 @@ +export { default } from "./queryHandler.js"; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/dist/src/middlewares/queryHandler/index.js.map b/dist/src/middlewares/queryHandler/index.js.map new file mode 100644 index 0000000..2274983 --- /dev/null +++ b/dist/src/middlewares/queryHandler/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/middlewares/queryHandler/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC"} \ No newline at end of file diff --git a/dist/src/middlewares/queryHandler/metadataHandler.d.ts b/dist/src/middlewares/queryHandler/metadataHandler.d.ts new file mode 100644 index 0000000..53a3eae --- /dev/null +++ b/dist/src/middlewares/queryHandler/metadataHandler.d.ts @@ -0,0 +1,17 @@ +import type { Request, Response } from 'express'; +import NodeCache from '@cacheable/node-cache'; +import { QueryParameters } from '../../types.js'; +export interface MetadataHandler { + cache: NodeCache; + metadataParameter: QueryParameters['metadata']; + req: Request; + res: Response; +} +/** + * + * @param req + * @param res + * @param cache + * @param metadataParameter + */ +export default function metadataHandler(req: Request, res: Response, cache: NodeCache, metadataParameter: QueryParameters['metadata']): void; diff --git a/dist/src/middlewares/queryHandler/metadataHandler.js b/dist/src/middlewares/queryHandler/metadataHandler.js new file mode 100644 index 0000000..8d3348d --- /dev/null +++ b/dist/src/middlewares/queryHandler/metadataHandler.js @@ -0,0 +1,50 @@ +import { generateEtag, getQueryParameterArray, getValueOrFirstEntry, } from '../../lib/utils.js'; +/** + * + * @param req + * @param res + * @param cache + * @param metadataParameter + */ +export default function metadataHandler(req, res, cache, metadataParameter) { + const metadataToMatch = getQueryParameterArray(req.query[metadataParameter]); + const searchTermsToMatch = [ + ...getQueryParameterArray(req.query.search), + ...getQueryParameterArray(req.query.s), + ]; + const metadata = cache.get('metadata') ?? {}; + const wantsJson = req.is('json') || + getValueOrFirstEntry(getQueryParameterArray(req.query.format)) === 'json'; + if (wantsJson) + res.set('content-type', 'application/json'); + else + res.set('content-type', 'text/plain'); + const matchedMetadata = Object.keys(metadata) + .filter((key) => (!metadataToMatch.length || + (metadataToMatch.length === 1 && metadataToMatch[0] === '') || + metadataToMatch.includes(key)) && + (!searchTermsToMatch.length || + searchTermsToMatch.some((term) => key.includes(term) || Array.isArray(metadata[key]) + ? metadata[key].some((val) => val.includes(term)) + : metadata[key].includes(term)))) + .reduce((acc, key) => { + const value = metadata[key]; + acc[key] = Array.isArray(value) + ? value.filter((value) => !searchTermsToMatch.length || + searchTermsToMatch.some((term) => key.includes(term) || value.includes(term))) + : value; + return acc; + }, {}); + const result = wantsJson + ? JSON.stringify(matchedMetadata) + : Object.keys(matchedMetadata) + .map((key) => { + const value = matchedMetadata[key]; + return Array.isArray(value) + ? value.map((rowVal) => `${key}: ${rowVal}`).join('\n') + : `${key}: ${value}`; + }) + .join('\n'); + res.set('etag', generateEtag(result)).send(result); +} +//# sourceMappingURL=metadataHandler.js.map \ No newline at end of file diff --git a/dist/src/middlewares/queryHandler/metadataHandler.js.map b/dist/src/middlewares/queryHandler/metadataHandler.js.map new file mode 100644 index 0000000..4d9f63c --- /dev/null +++ b/dist/src/middlewares/queryHandler/metadataHandler.js.map @@ -0,0 +1 @@ +{"version":3,"file":"metadataHandler.js","sourceRoot":"","sources":["../../../../src/middlewares/queryHandler/metadataHandler.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/src/middlewares/queryHandler/queryHandler.d.ts b/dist/src/middlewares/queryHandler/queryHandler.d.ts new file mode 100644 index 0000000..16d3a7f --- /dev/null +++ b/dist/src/middlewares/queryHandler/queryHandler.d.ts @@ -0,0 +1,11 @@ +import NodeCache from '@cacheable/node-cache'; +import type { NextFunction, Request, Response } from 'express'; +import { TwtKprConfiguration } from '../../types.js'; +/** + * + * @param config + * @param cache + * @param verifyAuthRequest + * @returns + */ +export default function queryHandler(config: TwtKprConfiguration, cache: NodeCache, verifyAuthRequest: (r: Request) => Promise): (req: Request, res: Response, next: NextFunction) => Promise; diff --git a/dist/src/middlewares/queryHandler/queryHandler.js b/dist/src/middlewares/queryHandler/queryHandler.js new file mode 100644 index 0000000..4073aa6 --- /dev/null +++ b/dist/src/middlewares/queryHandler/queryHandler.js @@ -0,0 +1,58 @@ +import path from 'node:path'; +import Debug from 'debug'; +import { __dirname } from '../../lib/constants.js'; +import { generateEtag } from '../../lib/utils.js'; +import renderApp from '../renderApp/index.js'; +import followingHandler from './followingHandler.js'; +import metadataHandler from './metadataHandler.js'; +import twtHandler from './twtHandler.js'; +const debug = Debug('twtkpr:queryHandler'); +/** + * + * @param config + * @param cache + * @param verifyAuthRequest + * @returns + */ +export default function queryHandler(config, cache, verifyAuthRequest) { + const { mainRoute, queryParameters, uploadConfiguration } = config; + return async (req, res, next) => { + debug({ query: JSON.stringify(req.query) }); + if (!Object.keys(req.query).length) { + next(); + return; + } + if (req.query[queryParameters.app] !== undefined) { + const appContent = renderApp({ mainRoute, uploadConfiguration }); + res.set('etag', generateEtag(appContent)).send(appContent); + return; + } + if (req.query[queryParameters.css] !== undefined) { + res.sendFile('styles.css', { + root: path.resolve(__dirname, 'client'), + }); + return; + } + if (req.query[queryParameters.js] !== undefined) { + res.sendFile('script.js', { + root: path.resolve(__dirname, 'client'), + }); + return; + } + if (req.query[queryParameters.following] !== undefined && + cache.get('following')) { + return followingHandler(req, res, cache, queryParameters.following); + } + if (req.query[queryParameters.metadata] !== undefined && + cache.get('metadata')) { + return metadataHandler(req, res, cache, queryParameters.metadata); + } + if ((req.query[queryParameters.twt] !== undefined || + req.query[queryParameters.twts] !== undefined) && + cache.get('twts')) { + return twtHandler(req, res, cache.get('twts'), queryParameters.twt, queryParameters.twts); + } + next(); + }; +} +//# sourceMappingURL=queryHandler.js.map \ No newline at end of file diff --git a/dist/src/middlewares/queryHandler/queryHandler.js.map b/dist/src/middlewares/queryHandler/queryHandler.js.map new file mode 100644 index 0000000..8499207 --- /dev/null +++ b/dist/src/middlewares/queryHandler/queryHandler.js.map @@ -0,0 +1 @@ +{"version":3,"file":"queryHandler.js","sourceRoot":"","sources":["../../../../src/middlewares/queryHandler/queryHandler.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,OAAO,SAAS,MAAM,uBAAuB,CAAC;AAC9C,OAAO,gBAAgB,MAAM,uBAAuB,CAAC;AACrD,OAAO,eAAe,MAAM,sBAAsB,CAAC;AACnD,OAAO,UAAU,MAAM,iBAAiB,CAAC;AAGzC,MAAM,KAAK,GAAG,KAAK,CAAC,qBAAqB,CAAC,CAAC;AAE3C;;;;;;GAMG;AACH,MAAM,CAAC,OAAO,UAAU,YAAY,CACnC,MAA2B,EAC3B,KAAyB,EACzB,iBAAmD;IAEnD,MAAM,EAAE,SAAS,EAAE,eAAe,EAAE,mBAAmB,EAAE,GAAG,MAAM,CAAC;IAEnE,OAAO,KAAK,EAAE,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;QAChE,KAAK,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAE5C,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,CAAC;YACpC,IAAI,EAAE,CAAC;YACP,OAAO;QACR,CAAC;QAED,IAAI,GAAG,CAAC,KAAK,CAAC,eAAe,CAAC,GAAG,CAAC,KAAK,SAAS,EAAE,CAAC;YAClD,MAAM,UAAU,GAAG,SAAS,CAAC,EAAE,SAAS,EAAE,mBAAmB,EAAE,CAAC,CAAC;YACjE,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAC3D,OAAO;QACR,CAAC;QAED,IAAI,GAAG,CAAC,KAAK,CAAC,eAAe,CAAC,GAAG,CAAC,KAAK,SAAS,EAAE,CAAC;YAClD,GAAG,CAAC,QAAQ,CAAC,YAAY,EAAE;gBAC1B,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,QAAQ,CAAC;aACvC,CAAC,CAAC;YACH,OAAO;QACR,CAAC;QAED,IAAI,GAAG,CAAC,KAAK,CAAC,eAAe,CAAC,EAAE,CAAC,KAAK,SAAS,EAAE,CAAC;YACjD,GAAG,CAAC,QAAQ,CAAC,WAAW,EAAE;gBACzB,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,QAAQ,CAAC;aACvC,CAAC,CAAC;YACH,OAAO;QACR,CAAC;QAED,IACC,GAAG,CAAC,KAAK,CAAC,eAAe,CAAC,SAAS,CAAC,KAAK,SAAS;YAClD,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,EACrB,CAAC;YACF,OAAO,gBAAgB,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,eAAe,CAAC,SAAS,CAAC,CAAC;QACrE,CAAC;QAED,IACC,GAAG,CAAC,KAAK,CAAC,eAAe,CAAC,QAAQ,CAAC,KAAK,SAAS;YACjD,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,EACpB,CAAC;YACF,OAAO,eAAe,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,eAAe,CAAC,QAAQ,CAAC,CAAC;QACnE,CAAC;QAED,IACC,CAAC,GAAG,CAAC,KAAK,CAAC,eAAe,CAAC,GAAG,CAAC,KAAK,SAAS;YAC5C,GAAG,CAAC,KAAK,CAAC,eAAe,CAAC,IAAI,CAAC,KAAK,SAAS,CAAC;YAC/C,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,EAChB,CAAC;YACF,OAAO,UAAU,CAChB,GAAG,EACH,GAAG,EACH,KAAK,CAAC,GAAG,CAAC,MAAM,CAAU,EAC1B,eAAe,CAAC,GAAG,EACnB,eAAe,CAAC,IAAI,CACpB,CAAC;QACH,CAAC;QAED,IAAI,EAAE,CAAC;IACR,CAAC,CAAC;AACH,CAAC"} \ No newline at end of file diff --git a/dist/src/middlewares/queryHandler/twtHandler.d.ts b/dist/src/middlewares/queryHandler/twtHandler.d.ts new file mode 100644 index 0000000..49288c5 --- /dev/null +++ b/dist/src/middlewares/queryHandler/twtHandler.d.ts @@ -0,0 +1,13 @@ +import type { Request, Response } from 'express'; +import type { Twt } from 'twtxt-lib'; +import { QueryParameters } from '../../types.js'; +/** + * + * @param req + * @param res + * @param twts + * @param twtParameter + * @param twtsParameter + * @returns + */ +export default function twtHandler(req: Request, res: Response, twts: Twt[] | undefined, twtParameter: QueryParameters['twt'], twtsParameter: QueryParameters['twts']): void; diff --git a/dist/src/middlewares/queryHandler/twtHandler.js b/dist/src/middlewares/queryHandler/twtHandler.js new file mode 100644 index 0000000..e9a5b80 --- /dev/null +++ b/dist/src/middlewares/queryHandler/twtHandler.js @@ -0,0 +1,68 @@ +import { generateEtag, getQueryParameterArray, getValueOrFirstEntry, } from '../../lib/utils.js'; +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc.js'; +dayjs.extend(utc); +/** + * + * @param req + * @param res + * @param twts + * @param twtParameter + * @param twtsParameter + * @returns + */ +export default function twtHandler(req, res, twts = [], twtParameter, twtsParameter) { + const twtsToMatch = getQueryParameterArray(req.query[twtsParameter]); + const showLastTwt = getQueryParameterArray(req.query[twtParameter]); + const hashesToMatch = getQueryParameterArray(req.query.hash); + const searchTermsToMatch = [ + ...getQueryParameterArray(req.query.search), + ...getQueryParameterArray(req.query.s), + ]; + const createdDatesToMatch = getQueryParameterArray(req.query.created_date); + const createdUTCStartDatesToMatch = getQueryParameterArray(req.query.created_date_start).map((val) => dayjs.utc(val)); + const createdUTCEndDatesToMatch = getQueryParameterArray(req.query.created_date_end).map((val) => dayjs.utc(val)); + const wantsJson = req.is('json') || + getValueOrFirstEntry(getQueryParameterArray(req.query.format)) === 'json'; + if (wantsJson) + res.set('content-type', 'application/json'); + else + res.set('content-type', 'text/plain'); + if (showLastTwt.length === 1 && showLastTwt[0] === '') { + const lastTwt = twts.reduce((matched, curr) => matched?.createdUTC > curr.createdUTC ? matched : curr); + let result = 'No results'; + if (lastTwt) { + result = wantsJson + ? JSON.stringify(lastTwt) + : `${lastTwt?.created || ''}\t${lastTwt?.content || ''}\n`; + } + res.set('etag', generateEtag(result)).send(result); + return; + } + const matchedTwts = twts.filter(({ content, created, createdUTC, hash }) => { + return ((!twtsToMatch.length || + (twtsToMatch.length === 1 && twtsToMatch[0] === '') || + twtsToMatch.includes(created) || + (hash && + (twtsToMatch.includes(hash) || twtsToMatch.includes(`#${hash}`)))) && + (!hashesToMatch.length || + (hash && + (hashesToMatch.includes(hash) || + hashesToMatch.includes(`#${hash}`)))) && + (!createdDatesToMatch.length || + createdDatesToMatch.some((date) => created.includes(date))) && + (!createdUTCStartDatesToMatch.length || + createdUTCStartDatesToMatch.some((date) => date.diff(createdUTC) < 0)) && + (!createdUTCEndDatesToMatch.length || + createdUTCEndDatesToMatch.some((date) => date.diff(createdUTC) > 0)) && + (!searchTermsToMatch.length || + searchTermsToMatch.some((term) => content.includes(term)))); + }); + const result = wantsJson + ? JSON.stringify(matchedTwts) + : matchedTwts + .map(({ content, created }) => `${created}\t${content}`) + .join('\n'); + res.set('etag', generateEtag(result)).send(result); +} +//# sourceMappingURL=twtHandler.js.map \ No newline at end of file diff --git a/dist/src/middlewares/queryHandler/twtHandler.js.map b/dist/src/middlewares/queryHandler/twtHandler.js.map new file mode 100644 index 0000000..a97a5c3 --- /dev/null +++ b/dist/src/middlewares/queryHandler/twtHandler.js.map @@ -0,0 +1 @@ +{"version":3,"file":"twtHandler.js","sourceRoot":"","sources":["../../../../src/middlewares/queryHandler/twtHandler.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/src/middlewares/renderApp/index.d.ts b/dist/src/middlewares/renderApp/index.d.ts new file mode 100644 index 0000000..4be966c --- /dev/null +++ b/dist/src/middlewares/renderApp/index.d.ts @@ -0,0 +1 @@ +export { default } from "./renderApp.js"; diff --git a/dist/src/middlewares/renderApp/index.js b/dist/src/middlewares/renderApp/index.js new file mode 100644 index 0000000..1759c73 --- /dev/null +++ b/dist/src/middlewares/renderApp/index.js @@ -0,0 +1,2 @@ +export { default } from "./renderApp.js"; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/dist/src/middlewares/renderApp/index.js.map b/dist/src/middlewares/renderApp/index.js.map new file mode 100644 index 0000000..1ce1ad5 --- /dev/null +++ b/dist/src/middlewares/renderApp/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/middlewares/renderApp/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC"} \ No newline at end of file diff --git a/dist/src/middlewares/renderApp/renderApp.d.ts b/dist/src/middlewares/renderApp/renderApp.d.ts new file mode 100644 index 0000000..9b29ec0 --- /dev/null +++ b/dist/src/middlewares/renderApp/renderApp.d.ts @@ -0,0 +1,7 @@ +import { TwtKprConfiguration } from '../../types.js'; +/** + * + * @param param0 + * @returns + */ +export default function renderApp({ mainRoute, uploadConfiguration, }: Pick): string; diff --git a/dist/src/middlewares/renderApp/renderApp.js b/dist/src/middlewares/renderApp/renderApp.js new file mode 100644 index 0000000..ce61094 --- /dev/null +++ b/dist/src/middlewares/renderApp/renderApp.js @@ -0,0 +1,143 @@ +import { version } from '../../packageInfo.js'; +import renderUploadButton from './renderUploadButton.js'; +/** + * + * @param param0 + * @returns + */ +export default function renderApp({ mainRoute, uploadConfiguration, }) { + return ` + + + + + + + + + + + + TwtKpr + + + + + +
+
+ + +
+

+            
+ +
+ + +
+ + +
+
+ + + + +`; +} +//# sourceMappingURL=renderApp.js.map \ No newline at end of file diff --git a/dist/src/middlewares/renderApp/renderApp.js.map b/dist/src/middlewares/renderApp/renderApp.js.map new file mode 100644 index 0000000..8c667cd --- /dev/null +++ b/dist/src/middlewares/renderApp/renderApp.js.map @@ -0,0 +1 @@ +{"version":3,"file":"renderApp.js","sourceRoot":"","sources":["../../../../src/middlewares/renderApp/renderApp.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAG/C,OAAO,kBAAkB,MAAM,yBAAyB,CAAC;AAEzD;;;;GAIG;AACH,MAAM,CAAC,OAAO,UAAU,SAAS,CAAC,EACjC,SAAS,EACT,mBAAmB,GAC6C;IAChE,OAAO;;;;;;;;;;;;;;mCAc2B,SAAS;;;;;;;;8CAQE,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;uEAyCgB,SAAS;;;;sCAI1C,OAAO,IAAI,SAAS;;;;;;;;;;;;0BAYhC,kBAAkB,CAAC,mBAAmB,CAAC;;;;;;;;;;;;;;8CAcnB,OAAO,IAAI,SAAS;;kCAEhC,kBAAkB,CAAC,mBAAmB,EAAE,OAAO,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iCAiCjD,SAAS;;;;CAIzC,CAAC;AACF,CAAC"} \ No newline at end of file diff --git a/dist/src/middlewares/renderApp/renderUploadButton.d.ts b/dist/src/middlewares/renderApp/renderUploadButton.d.ts new file mode 100644 index 0000000..c92abbb --- /dev/null +++ b/dist/src/middlewares/renderApp/renderUploadButton.d.ts @@ -0,0 +1,8 @@ +import { TwtKprConfiguration } from '../../types.js'; +/** + * + * @param uploadConfiguration + * @param variant + * @returns + */ +export default function renderUploadButton(uploadConfiguration: TwtKprConfiguration['uploadConfiguration'], variant?: 'normal' | 'small'): string; diff --git a/dist/src/middlewares/renderApp/renderUploadButton.js b/dist/src/middlewares/renderApp/renderUploadButton.js new file mode 100644 index 0000000..7845039 --- /dev/null +++ b/dist/src/middlewares/renderApp/renderUploadButton.js @@ -0,0 +1,23 @@ +/** + * + * @param uploadConfiguration + * @param variant + * @returns + */ +export default function renderUploadButton(uploadConfiguration, variant = 'normal') { + const { active, allowedMimeTypes, route } = uploadConfiguration ?? {}; + if (!active) + return ''; + // determine accept from allowed mime types - may need to rebuild value based on fallback n getConfiguration, rather than at the end. + return ` + +`; +} +//# sourceMappingURL=renderUploadButton.js.map \ No newline at end of file diff --git a/dist/src/middlewares/renderApp/renderUploadButton.js.map b/dist/src/middlewares/renderApp/renderUploadButton.js.map new file mode 100644 index 0000000..6a4daa4 --- /dev/null +++ b/dist/src/middlewares/renderApp/renderUploadButton.js.map @@ -0,0 +1 @@ +{"version":3,"file":"renderUploadButton.js","sourceRoot":"","sources":["../../../../src/middlewares/renderApp/renderUploadButton.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,MAAM,CAAC,OAAO,UAAU,kBAAkB,CACzC,mBAA+D,EAC/D,UAA8B,QAAQ;IAEtC,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,KAAK,EAAE,GAAG,mBAAmB,IAAI,EAAE,CAAC;IAEtE,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,CAAC;IAEvB,qIAAqI;IAErI,OAAO;yFACiF,OAAO;0CACtD,OAAO;;oBAE7B,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG;4EACmB,KAAK;6CACpC,OAAO;;;CAGnD,CAAC;AACF,CAAC"} \ No newline at end of file diff --git a/dist/src/middlewares/uploadHandler.d.ts b/dist/src/middlewares/uploadHandler.d.ts new file mode 100644 index 0000000..b8c5e16 --- /dev/null +++ b/dist/src/middlewares/uploadHandler.d.ts @@ -0,0 +1,9 @@ +import type { NextFunction, Request, Response } from 'express'; +import { TwtKprConfiguration } from '../types.js'; +/** + * + * @param config + * @param verifyAuthRequest + * @returns + */ +export default function uploadHandler(config: TwtKprConfiguration, verifyAuthRequest: (r: Request) => Promise): (req: Request, res: Response, next: NextFunction) => Promise; diff --git a/dist/src/middlewares/uploadHandler.js b/dist/src/middlewares/uploadHandler.js new file mode 100644 index 0000000..e3a21a3 --- /dev/null +++ b/dist/src/middlewares/uploadHandler.js @@ -0,0 +1,129 @@ +import fsp from 'node:fs/promises'; +import path from 'node:path'; +import formidable from 'formidable'; +import Debug from 'debug'; +const debug = Debug('twtkpr:uploadHandler'); +/** + * + * @param allowedMimeTypes + * @returns + */ +const getDestinationByMimeTypeConfiguration = (allowedMimeTypes) => { + const fallback = { + audio: 'audio', + image: 'images', + text: 'texts', + video: 'videos', + '*': 'files', + }; + const mimeTypeArrayReducer = (acc, curr) => { + if (fallback[curr]) + acc[curr] = fallback[curr]; + else + acc[curr] = `${curr}s`; + return acc; + }; + if (!allowedMimeTypes) + return fallback; + if (typeof allowedMimeTypes === 'string') + return allowedMimeTypes + .split(',') + .map((val) => val.trim()) + .reduce(mimeTypeArrayReducer, {}); + if (Array.isArray(allowedMimeTypes)) + return allowedMimeTypes.reduce(mimeTypeArrayReducer, {}); + if (typeof allowedMimeTypes === 'object') + return allowedMimeTypes; + return fallback; +}; +/** + * + * @param config + * @param verifyAuthRequest + * @returns + */ +export default function uploadHandler(config, verifyAuthRequest) { + return async (req, res, next) => { + debug('checking auth'); + if (!(await verifyAuthRequest(req))) { + debug('auth check failed'); + res.status(401).send('Unauthorized'); + return; + } + debug('auth check succeeded'); + const { active, allowedMimeTypes, directory, route, ...otherProps } = config.uploadConfiguration; + if (!active || + (Array.isArray(allowedMimeTypes) && !allowedMimeTypes.length)) { + next(); + return; + } + debug('using configuration: ', { + uploadConfiguration: config.uploadConfiguration, + }); + const form = formidable({ + uploadDir: directory, + ...otherProps, + }); + form.parse(req, async (err, fields, files) => { + if (err) { + next(err); + return; + } + const uploadsDir = (route ?? '').replaceAll('/', ''); + let hadFileError = false; + const processedFiles = []; + const destinationByMimeType = allowedMimeTypes; + debug(`processing ${(files?.files ?? []).length} files`); + for (const file of files?.files ?? []) { + const { filepath, hash, mimetype, newFilename, originalFilename } = file ?? {}; + if (!(filepath && newFilename && originalFilename)) + return; + console.log({ file }); + let ext = path.extname(originalFilename).toLocaleLowerCase(); + if (ext === '.jpeg') + ext = '.jpg'; + const finalFilename = (hash && (mimetype?.includes('image') || mimetype?.includes('video')) + ? `${hash}${ext}` + : originalFilename) + .replace(/\s+/g, '-') + .toLocaleLowerCase(); + let destinationDir = ''; + Object.keys(destinationByMimeType).forEach((mimeType) => { + if (file.mimetype?.split('/')?.[0] === mimeType.toLocaleLowerCase()) + destinationDir = + destinationByMimeType[mimeType].directory ?? ''; + }); + if (destinationDir === '') + destinationDir = + destinationByMimeType['*'].directory ?? uploadsDir; + const finalPath = path.join(process.cwd(), 'public', destinationDir); + debug(`creating '${finalPath}'`); + fsp.mkdir(finalPath, { recursive: true }); + debug(`copying '${filepath}' to '/${destinationDir}/${finalFilename}'`); + try { + await fsp.copyFile(filepath, path.join(finalPath, finalFilename)); + debug(`cleaning up '${filepath}'`); + await fsp.rm(filepath); + debug(`processed successfully`); + processedFiles.push(`/${destinationDir}/${finalFilename}`); + } + catch (err) { + debug(`error!`); + hadFileError = true; + console.error(err); + } + } + debug('generating reply...'); + if (hadFileError && processedFiles.length) { + res.type('text/plain').status(206).send(processedFiles.join('\n')); + return; + } + if (!processedFiles.length) { + res.type('text/plain').status(500).send('No files processed'); + return; + } + res.type('text/plain').status(201).send(processedFiles.join('\n')); + }); + }; +} +//# sourceMappingURL=uploadHandler.js.map \ No newline at end of file diff --git a/dist/src/middlewares/uploadHandler.js.map b/dist/src/middlewares/uploadHandler.js.map new file mode 100644 index 0000000..f77f643 --- /dev/null +++ b/dist/src/middlewares/uploadHandler.js.map @@ -0,0 +1 @@ +{"version":3,"file":"uploadHandler.js","sourceRoot":"","sources":["../../../src/middlewares/uploadHandler.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,kBAAkB,CAAC;AACnC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,UAAU,MAAM,YAAY,CAAC;AAEpC,OAAO,KAAK,MAAM,OAAO,CAAC;AAK1B,MAAM,KAAK,GAAG,KAAK,CAAC,sBAAsB,CAAC,CAAC;AAE5C;;;;GAIG;AACH,MAAM,qCAAqC,GAAG,CAC7C,gBAA6D,EAC5D,EAAE;IACH,MAAM,QAAQ,GAA2B;QACxC,KAAK,EAAE,OAAO;QACd,KAAK,EAAE,QAAQ;QACf,IAAI,EAAE,OAAO;QACb,KAAK,EAAE,QAAQ;QACf,GAAG,EAAE,OAAO;KACZ,CAAC;IAEF,MAAM,oBAAoB,GAAG,CAAC,GAA2B,EAAE,IAAY,EAAE,EAAE;QAC1E,IAAI,QAAQ,CAAC,IAAI,CAAC;YAAE,GAAG,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;;YAC1C,GAAG,CAAC,IAAI,CAAC,GAAG,GAAG,IAAI,GAAG,CAAC;QAC5B,OAAO,GAAG,CAAC;IACZ,CAAC,CAAC;IAEF,IAAI,CAAC,gBAAgB;QAAE,OAAO,QAAQ,CAAC;IAEvC,IAAI,OAAO,gBAAgB,KAAK,QAAQ;QACvC,OAAO,gBAAgB;aACrB,KAAK,CAAC,GAAG,CAAC;aACV,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;aACxB,MAAM,CAAC,oBAAoB,EAAE,EAAE,CAAC,CAAC;IAEpC,IAAI,KAAK,CAAC,OAAO,CAAC,gBAAgB,CAAC;QAClC,OAAQ,gBAA6B,CAAC,MAAM,CAAC,oBAAoB,EAAE,EAAE,CAAC,CAAC;IAExE,IAAI,OAAO,gBAAgB,KAAK,QAAQ;QAAE,OAAO,gBAAgB,CAAC;IAElE,OAAO,QAAQ,CAAC;AACjB,CAAC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,CAAC,OAAO,UAAU,aAAa,CACpC,MAA2B,EAC3B,iBAAmD;IAEnD,OAAO,KAAK,EAAE,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;QAChE,KAAK,CAAC,eAAe,CAAC,CAAC;QACvB,IAAI,CAAC,CAAC,MAAM,iBAAiB,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;YACrC,KAAK,CAAC,mBAAmB,CAAC,CAAC;YAC3B,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;YACrC,OAAO;QACR,CAAC;QACD,KAAK,CAAC,sBAAsB,CAAC,CAAC;QAE9B,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,UAAU,EAAE,GAClE,MAAM,CAAC,mBAAmB,CAAC;QAE5B,IACC,CAAC,MAAM;YACP,CAAC,KAAK,CAAC,OAAO,CAAC,gBAAgB,CAAC,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,EAC5D,CAAC;YACF,IAAI,EAAE,CAAC;YACP,OAAO;QACR,CAAC;QAED,KAAK,CAAC,uBAAuB,EAAE;YAC9B,mBAAmB,EAAE,MAAM,CAAC,mBAAmB;SAC/C,CAAC,CAAC;QAEH,MAAM,IAAI,GAAG,UAAU,CAAC;YACvB,SAAS,EAAE,SAAS;YACpB,GAAG,UAAU;SACb,CAAC,CAAC;QAEH,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE;YAC5C,IAAI,GAAG,EAAE,CAAC;gBACT,IAAI,CAAC,GAAG,CAAC,CAAC;gBACV,OAAO;YACR,CAAC;YACD,MAAM,UAAU,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,UAAU,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;YAErD,IAAI,YAAY,GAAG,KAAK,CAAC;YACzB,MAAM,cAAc,GAAa,EAAE,CAAC;YACpC,MAAM,qBAAqB,GAAG,gBAAgB,CAAC;YAE/C,KAAK,CAAC,cAAc,CAAC,KAAK,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,MAAM,QAAQ,CAAC,CAAC;YAEzD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,KAAK,IAAI,EAAE,EAAE,CAAC;gBACvC,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,gBAAgB,EAAE,GAChE,IAAI,IAAI,EAAE,CAAC;gBACZ,IAAI,CAAC,CAAC,QAAQ,IAAI,WAAW,IAAI,gBAAgB,CAAC;oBAAE,OAAO;gBAE3D,OAAO,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC;gBAEtB,IAAI,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC,iBAAiB,EAAE,CAAC;gBAC7D,IAAI,GAAG,KAAK,OAAO;oBAAE,GAAG,GAAG,MAAM,CAAC;gBAElC,MAAM,aAAa,GAAG,CACrB,IAAI,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,OAAO,CAAC,IAAI,QAAQ,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;oBACnE,CAAC,CAAC,GAAG,IAAI,GAAG,GAAG,EAAE;oBACjB,CAAC,CAAC,gBAAgB,CACnB;qBACC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;qBACpB,iBAAiB,EAAE,CAAC;gBAEtB,IAAI,cAAc,GAAG,EAAE,CAAC;gBACxB,MAAM,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,EAAE;oBACvD,IAAI,IAAI,CAAC,QAAQ,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,QAAQ,CAAC,iBAAiB,EAAE;wBAClE,cAAc;4BAEZ,qBAAqB,CACpB,QAA8C,CAE/C,CAAC,SAAS,IAAI,EAAE,CAAC;gBACrB,CAAC,CAAC,CAAC;gBACH,IAAI,cAAc,KAAK,EAAE;oBACxB,cAAc;wBAEZ,qBAAqB,CACpB,GAAyC,CAE1C,CAAC,SAAS,IAAI,UAAU,CAAC;gBAE5B,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,QAAQ,EAAE,cAAc,CAAC,CAAC;gBAErE,KAAK,CAAC,aAAa,SAAS,GAAG,CAAC,CAAC;gBACjC,GAAG,CAAC,KAAK,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;gBAE1C,KAAK,CAAC,YAAY,QAAQ,UAAU,cAAc,IAAI,aAAa,GAAG,CAAC,CAAC;gBAExE,IAAI,CAAC;oBACJ,MAAM,GAAG,CAAC,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC,CAAC;oBAElE,KAAK,CAAC,gBAAgB,QAAQ,GAAG,CAAC,CAAC;oBACnC,MAAM,GAAG,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC;oBAEvB,KAAK,CAAC,wBAAwB,CAAC,CAAC;oBAChC,cAAc,CAAC,IAAI,CAAC,IAAI,cAAc,IAAI,aAAa,EAAE,CAAC,CAAC;gBAC5D,CAAC;gBAAC,OAAO,GAAG,EAAE,CAAC;oBACd,KAAK,CAAC,QAAQ,CAAC,CAAC;oBAChB,YAAY,GAAG,IAAI,CAAC;oBACpB,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;gBACpB,CAAC;YACF,CAAC;YAED,KAAK,CAAC,qBAAqB,CAAC,CAAC;YAC7B,IAAI,YAAY,IAAI,cAAc,CAAC,MAAM,EAAE,CAAC;gBAC3C,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;gBACnE,OAAO;YACR,CAAC;YAED,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE,CAAC;gBAC5B,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;gBAC9D,OAAO;YACR,CAAC;YAED,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QACpE,CAAC,CAAC,CAAC;IACJ,CAAC,CAAC;AACH,CAAC"} \ No newline at end of file diff --git a/dist/src/packageInfo.d.ts b/dist/src/packageInfo.d.ts new file mode 100644 index 0000000..b8b28e1 --- /dev/null +++ b/dist/src/packageInfo.d.ts @@ -0,0 +1,6 @@ +declare const author: { + name: string; + email: string; + url: string; +}, name: string, version: string; +export { author, name, version }; diff --git a/dist/src/packageInfo.js b/dist/src/packageInfo.js new file mode 100644 index 0000000..d56f360 --- /dev/null +++ b/dist/src/packageInfo.js @@ -0,0 +1,4 @@ +import pkgInfo from "../package.json" with { type: "json" }; +const { author, name, version } = pkgInfo; +export { author, name, version }; +//# sourceMappingURL=packageInfo.js.map \ No newline at end of file diff --git a/dist/src/packageInfo.js.map b/dist/src/packageInfo.js.map new file mode 100644 index 0000000..0ec9b10 --- /dev/null +++ b/dist/src/packageInfo.js.map @@ -0,0 +1 @@ +{"version":3,"file":"packageInfo.js","sourceRoot":"","sources":["../../src/packageInfo.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,iBAAiB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAC;AAE5D,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;AAE1C,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC"} \ No newline at end of file diff --git a/dist/src/plugin.d.ts b/dist/src/plugin.d.ts new file mode 100644 index 0000000..a039c16 --- /dev/null +++ b/dist/src/plugin.d.ts @@ -0,0 +1,3 @@ +import { Router } from 'express'; +import { TwtKprPluginConfiguration } from './types.js'; +export default function plugin(initialConfig?: TwtKprPluginConfiguration): Router; diff --git a/dist/src/plugin.js b/dist/src/plugin.js new file mode 100644 index 0000000..ab1e8c0 --- /dev/null +++ b/dist/src/plugin.js @@ -0,0 +1,40 @@ +import cookieParser from 'cookie-parser'; +import Debug from 'debug'; +import express from 'express'; +import authCheck from './middlewares/authCheckJWT.js'; +import twtxtCache from './lib/twtxtCache.js'; +import getConfiguration from './lib/getConfiguration.js'; +import queryHandler from './middlewares/queryHandler/index.js'; +import uploadHandler from './middlewares/uploadHandler.js'; +import postHandler from './middlewares/postHandler/index.js'; +import memoryCache from './middlewares/postHandler/memoryCache.js'; +import putHandler from './middlewares/putHandler/index.js'; +export default function plugin(initialConfig) { + const debug = Debug('twtkpr:plugin'); + const router = express.Router(); + const config = getConfiguration(initialConfig ?? {}); + const { publicDirectory, twtxtFilename } = config; + const verifyAuthRequest = (req) => authCheck(req, config); + debug('initializing cache'); + const { cache, reloadCache } = twtxtCache({ publicDirectory, twtxtFilename }); + debug('adding URL encoder'); + router.use(express.urlencoded({ extended: true })); + debug('adding cookieParser'); + router.use(cookieParser()); + debug('adding queryRouter'); + router.use(config.mainRoute, queryHandler(config, cache, verifyAuthRequest)); + debug(`adding uploadHandler at /${config.uploadConfiguration.route}`); + router.post(`/${config.uploadConfiguration.route}`, uploadHandler(config, verifyAuthRequest)); + debug('adding postHandler and putHandler'); + router.use(config.mainRoute, postHandler(config), putHandler(config)); + debug('adding static'); + router.use(express.static(config.publicDirectory)); + debug('adding default redirect'); + router.get('/', (_, res) => { + res.redirect(config.mainRoute); + }); + debug('adding memoryCache'); + router.use((req, res, next) => memoryCache(req, res, next, cache, reloadCache)); + return router; +} +//# sourceMappingURL=plugin.js.map \ No newline at end of file diff --git a/dist/src/plugin.js.map b/dist/src/plugin.js.map new file mode 100644 index 0000000..1a3bbfd --- /dev/null +++ b/dist/src/plugin.js.map @@ -0,0 +1 @@ +{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,eAAe,CAAC;AACzC,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,OAA4B,MAAM,SAAS,CAAC;AAGnD,OAAO,SAAS,MAAM,+BAA+B,CAAC;AACtD,OAAO,UAAU,MAAM,qBAAqB,CAAC;AAC7C,OAAO,gBAAgB,MAAM,2BAA2B,CAAC;AACzD,OAAO,YAAY,MAAM,qCAAqC,CAAC;AAC/D,OAAO,aAAa,MAAM,gCAAgC,CAAC;AAC3D,OAAO,WAAW,MAAM,oCAAoC,CAAC;AAC7D,OAAO,WAAW,MAAM,0CAA0C,CAAC;AACnE,OAAO,UAAU,MAAM,mCAAmC,CAAC;AAE3D,MAAM,CAAC,OAAO,UAAU,MAAM,CAAC,aAAyC;IACvE,MAAM,KAAK,GAAG,KAAK,CAAC,eAAe,CAAC,CAAC;IAErC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAEhC,MAAM,MAAM,GAAG,gBAAgB,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC;IACrD,MAAM,EAAE,eAAe,EAAE,aAAa,EAAE,GAAG,MAAM,CAAC;IAElD,MAAM,iBAAiB,GAAG,CAAC,GAAY,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAEnE,KAAK,CAAC,oBAAoB,CAAC,CAAC;IAC5B,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,GAAG,UAAU,CAAC,EAAE,eAAe,EAAE,aAAa,EAAE,CAAC,CAAC;IAE9E,KAAK,CAAC,oBAAoB,CAAC,CAAC;IAC5B,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IAEnD,KAAK,CAAC,qBAAqB,CAAC,CAAC;IAC7B,MAAM,CAAC,GAAG,CAAC,YAAY,EAAE,CAAC,CAAC;IAE3B,KAAK,CAAC,oBAAoB,CAAC,CAAC;IAC5B,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,MAAM,EAAE,KAAK,EAAE,iBAAiB,CAAC,CAAC,CAAC;IAE7E,KAAK,CAAC,4BAA4B,MAAM,CAAC,mBAAmB,CAAC,KAAK,EAAE,CAAC,CAAC;IACtE,MAAM,CAAC,IAAI,CACV,IAAI,MAAM,CAAC,mBAAmB,CAAC,KAAK,EAAE,EACtC,aAAa,CAAC,MAAM,EAAE,iBAAiB,CAAC,CACxC,CAAC;IAEF,KAAK,CAAC,mCAAmC,CAAC,CAAC;IAC3C,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,EAAE,WAAW,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;IAEtE,KAAK,CAAC,eAAe,CAAC,CAAC;IACvB,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC,CAAC;IAEnD,KAAK,CAAC,yBAAyB,CAAC,CAAC;IACjC,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE;QAC1B,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IAChC,CAAC,CAAC,CAAC;IAEH,KAAK,CAAC,oBAAoB,CAAC,CAAC;IAC5B,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,CAC7B,WAAW,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,CAAC,CAC/C,CAAC;IAEF,OAAO,MAAgB,CAAC;AACzB,CAAC"} \ No newline at end of file diff --git a/dist/src/types.d.ts b/dist/src/types.d.ts new file mode 100644 index 0000000..0cecdce --- /dev/null +++ b/dist/src/types.d.ts @@ -0,0 +1,46 @@ +import { Options } from 'express-rate-limit'; +import formidable from 'formidable'; +export interface MimeOptions { + directory?: string; + rename?: boolean; +} +export interface MimeImageOptions extends MimeOptions { + compression?: string; + maxHeight?: string; + maxWidth?: string; +} +export interface UploadConfiguration extends Partial> { + active: boolean; + directory: string; + allowedMimeTypes: string | string[] | Record; + route: string; +} +export interface QueryParameters { + app: string; + css: string; + following: string; + js: string; + logout: string; + metadata: string; + twt: string; + twts: string; +} +export interface PostLimiterConfiguration extends Partial { + active: boolean; +} +export interface TwtKprConfiguration { + accessSecret: string; + mainRoute: string; + privateDirectory: string; + publicDirectory: string; + refreshSecret: string; + twtxtFilename: string; + postLimiterConfiguration?: PostLimiterConfiguration; + queryParameters: QueryParameters; + uploadConfiguration: UploadConfiguration; +} +export interface TwtKprPluginConfiguration extends Omit, 'postLimiterConfiguration' | 'queryParameters' | 'uploadConfiguration'> { + postLimiterConfiguration?: Partial; + queryParameters?: Partial; + uploadConfiguration?: Partial; +} diff --git a/dist/src/types.js b/dist/src/types.js new file mode 100644 index 0000000..718fd38 --- /dev/null +++ b/dist/src/types.js @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=types.js.map \ No newline at end of file diff --git a/dist/src/types.js.map b/dist/src/types.js.map new file mode 100644 index 0000000..7b5fff8 --- /dev/null +++ b/dist/src/types.js.map @@ -0,0 +1 @@ +{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..57dc71c --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,42 @@ +export default [ + { + // Basic settings + files: ['**/*.js', '**/*.ts'], + ignores: ['dist/**', 'node_modules/**'], + languageOptions: { + ecmaVersion: 2025, + sourceType: 'module', + }, + + // Core rules + rules: { + 'no-unused-vars': 'error', + 'no-undef': 'error', + 'no-console': 'warn', + + 'node/no-deprecated-api': 'error', + 'node/no-missing-import': 'error', + 'performance/no-costly-loop': 'warn', + 'promise/prefer-await-to-then': 'error', + 'security/detect-eval-with-expression': 'error', + 'security/detect-non-literal-fs-filename': 'error', + 'security/detect-non-literal-regexp': 'error', + 'security/detect-object-injection': 'warn', + 'security/detect-possible-timing-attacks': 'warn', + }, + + // Use recommended presets + extends: [ + 'eslint:recommended', + 'plugin:node/recommended', + 'plugin:security/recommended', + ], + }, + { + files: ['**/*.js', '**/*.ts'], + plugins: ['prettier'], + rules: { + 'prettier/prettier': 'error', + }, + }, +]; diff --git a/package.json b/package.json new file mode 100644 index 0000000..c5b60da --- /dev/null +++ b/package.json @@ -0,0 +1,81 @@ +{ + "name": "express-twtkpr", + "version": "0.8.0", + "description": "An express library for hosting and maintaining a twtxt.txt file.", + "license": "MIT", + "author": { + "name": "Eric Woodward", + "email": "hey@itsericwoodward.com", + "url": "https://www.itsericwoodward.com" + }, + "repository": { + "type": "git", + "url": "https://git.itsericwoodward.com/eric/express-twtkp" + }, + "keywords": [], + "type": "module", + "main": "./dist/src/index.js", + "module": "./dist/src/index.js", + "types": "./dist/src/index.d.ts", + "exports": { + ".": { + "import": "./dist/src/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "start": "node --env-file=.env dist/index-app.js", + "build": "tsc && cp -r src/client dist/src", + "dev": "DEBUG='twtkpr:*' tsx watch --env-file=.env src/index-app.ts", + "get:hash": "tsx --env-file=.env src/cli.ts get-hash", + "lint": "eslint --fix src test", + "prepublishOnly": "yarn build", + "set:user": "tsx --env-file=.env src/cli.ts set-user", + "test": "vitest", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@cacheable/node-cache": "^2.0.2", + "@exodus/blakejs": "^1.1.1-exodus.0", + "base32.js": "^0.1.0", + "bcryptjs": "^3.0.3", + "cookie-parser": "^1.4.7", + "dayjs": "^1.11.20", + "debug": "^4.4.3", + "express": "^5.2.1", + "express-rate-limit": "^8.3.1", + "express-session": "^1.19.0", + "express-slow-down": "^3.1.0", + "formidable": "^3.5.4", + "jsonwebtoken": "^9.0.3", + "link": "^2.1.2", + "session-file-store": "^1.5.0", + "twtxt-lib": "^0.9.4", + "uuid": "^13.0.0", + "zod": "^4.3.6" + }, + "devDependencies": { + "@types/cookie-parser": "^1.4.10", + "@types/cors": "^2.8.19", + "@types/debug": "^4.1.12", + "@types/express": "^5.0.6", + "@types/express-session": "^1.18.2", + "@types/formidable": "^3.5.0", + "@types/jsonwebtoken": "^9.0.10", + "@types/morgan": "^1.9.10", + "@types/node": "^25.5.0", + "@types/supertest": "^7.2.0", + "eslint": "^10.0.3", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.5", + "eslint-plugin-security": "^4.0.0", + "prettier": "^3.8.1", + "supertest": "^7.2.2", + "tsx": "^4.21.0", + "typescript": "^5.9.3", + "vitest": "^4.1.0" + }, + "packageManager": "yarn@4.13.0" +} diff --git a/src/client/script.js b/src/client/script.js new file mode 100644 index 0000000..7981616 --- /dev/null +++ b/src/client/script.js @@ -0,0 +1,506 @@ +const DEBUG_ON = true; + +// served from same path as TWTXT file +const TWTXT_FILE_URL = window.location.pathname; +const REMEMBER_LOGIN_STORAGE_KEY = 'rememberLogin'; +const ACCESS_TOKEN_COOKIE_KEY = 'accessToken'; + +const debug = (...vals) => { + if (DEBUG_ON) console.log(...vals); +}; + +export default (async () => { + /* DOM Elements */ + const twtForm = document.getElementById('twtForm'), + loginForm = document.getElementById('loginControls-form'), + fileBox = document.getElementById('fileBox'), + fileContentsSection = document.getElementById('fileContentsSection'), + toastContainer = document.getElementById('toast-container'), + twtControlsContentInput = document.getElementById( + 'twtControlsContentInput' + ), + twtSubmitButton = document.querySelector('.twtControls-submitButton'), + twtLogoutButton = document.getElementById('twtControlsLogoutButton'), + twtFileEditButton = document.getElementById('twtControlsEditButton'), + menuCheckbox = document.getElementById('hamburgerToggleCheckbox'), + twtxtEditFormText = document.getElementById('twtxtEditFormText'), + uploadInputs = document.querySelectorAll('.twtControls-uploadInput'), + rememberToggle = document.getElementById('loginControls-rememberToggle'); + + const lastModifiedDates = {}; + let isEditing = false, + cookie, + fileText, + token; + + const showToast = (message, type = 'success') => { + const toast = document.createElement('div'); + toast.classList.add('toast'); + if (type === 'error') toast.classList.add('error'); + toast.textContent = message; + + toastContainer.appendChild(toast); + setTimeout(() => { + toast.style.animation = 'fadeOut 0.5s forwards'; + setTimeout(() => toast.remove(), 500); + }, 3000); + }; + + const beginEditMode = () => { + isEditing = true; + + if (menuCheckbox) menuCheckbox.checked = false; + if (twtSubmitButton) twtSubmitButton.setAttribute('disabled', 'disabled'); + document.body.classList.add('js-editMode'); + }; + + const endEditMode = () => { + isEditing = false; + + if (twtSubmitButton) twtSubmitButton.removeAttribute('disabled'); + document.body.classList.remove('js-editMode'); + }; + + const getCookie = (name) => { + const cookies = document.cookie.split('; '); + for (const cookie of cookies) { + const [key, value] = cookie.split('='); + if (key === name) return decodeURIComponent(value); + } + return null; + }; + + const setCookie = (name, value, expireDays) => { + const isSecure = window.location.protocol === 'https'; + const expireDate = new Date(); // current date + expireDate.setTime( + expireDate.getTime() + (expireDays ?? 0) * 24 * 60 * 60 * 1000 + ); + let expires = + expireDays !== undefined ? `expires=${expireDate.toUTCString()}; ` : ''; + document.cookie = `${name}=${encodeURIComponent(value)}; ${expires}${isSecure ? 'Secure; ' : ''}SameSite=Strict; Path=/`; + }; + + const loadTwtxtFile = async (filePath = TWTXT_FILE_URL) => { + debug('loadTwtxtFile start'); + let response; + const fileURL = `${filePath.charAt(0) !== '/' ? '/' : ''}${filePath}`; + + const headers = new Headers(); + headers.set( + 'Accept', + 'application/twtxt+txt,text/twtxt,text/plain;q=0.9,*/*;q=0.8' + ); + + if (lastModifiedDates[fileURL]) { + headers['If-Modified-Since'] = lastModifiedDates[fileURL]; + } + + try { + response = await fetch(fileURL, { + method: 'GET', + headers, + mode: 'same-origin', + }); + + if (response?.body) fileText = await new Response(response.body).text(); + + if (response?.headers.get('Last-Modified')) { + lastModifiedDates[fileURL] = response?.headers.get('Last-Modified'); + } + + if (fileText && fileBox) { + fileBox.textContent = fileText; + } + } catch (err) { + showToast('Unable to load file, please try again later.', 'error'); + console.error('Error loading file', err); + } + debug('loadTwtxtFile end'); + }; + + const refreshToken = async (hideToast = false) => { + debug('refreshToken start', hideToast); + const rememberToggleVal = + localStorage.getItem(REMEMBER_LOGIN_STORAGE_KEY) === 'true'; + + const res = await fetch(`${TWTXT_FILE_URL}`, { + method: 'POST', + body: new URLSearchParams({ + rememberToggle: rememberToggleVal, + type: 'refresh', + }), + credentials: 'include', // Include cookies + }); + + if (res.ok && res?.body) { + token = await new Response(res.body).text(); + if (rememberToggleVal) { + debug('refreshToken set new accessToken cookie'); + setCookie(ACCESS_TOKEN_COOKIE_KEY, token); + } + debug('refreshToken end OK'); + return; + } + + // Handle refresh failure + if (!hideToast) + showToast('Unable to refresh token, please try again later.', 'error'); + token = undefined; + document.body.classList.remove('js-authorized'); + debug('refreshToken end error'); + throw new Error('Failed to refresh token'); + }; + + const uploadFiles = async (files, uploadRoute, secondAttempt = false) => { + if (!uploadRoute) return; + + debug('uploadFiles', token, files, uploadRoute, secondAttempt); + + const formData = new FormData(); + for (let i = 0; i < files.length; i++) { + formData.append('files', files[i]); + } + + try { + const res = await fetch(uploadRoute, { + method: 'POST', + body: formData, + headers: { + Authorization: `Bearer ${token}`, + }, + credentials: 'include', + }); + + if (res.ok) { + showToast(`File${files.length !== 1 ? 's' : ''} uploaded`); + + const filePath = await res.text(); + twtControlsContentInput.value += filePath + .split('\n') + .map((currFilePath) => + [ + ' ', + location.protocol, + '//', + location.hostname, + location.protocol !== 'https' && location.port !== 80 + ? ':' + location.port + : '', + currFilePath, + ].join('') + ) + .join(''); + + return; + } + + if (!secondAttempt) { + await refreshToken(); + return uploadFiles(files, uploadRoute, true); + } + + showToast( + `Unable to upload image${files.length !== 1 ? 's' : ''} refresh token, please try again later.`, + 'error' + ); + } catch (err) { + console.error(err); + } + }; + + /* Handlers */ + + const dragOverHandler = (ev) => { + const files = [...ev.dataTransfer.items].filter( + (item) => item.kind === 'file' + ); + + if (files.length > 0) { + ev.preventDefault(); + ev.dataTransfer.dropEffect = 'copy'; + } + }; + + const dragOverWindowHandler = (ev) => { + const files = [...ev.dataTransfer.items].filter( + (item) => item.kind === 'file' + ); + + if (files.length > 0) { + ev.preventDefault(); + + if (!twtControlsContentInput.contains(ev.target)) { + ev.dataTransfer.dropEffect = 'none'; + } + } + }; + + const dropHandler = (ev) => { + ev.preventDefault(); + + if (!uploadInputs.length) return; + + const files = [...ev.dataTransfer.items] + .map((item) => item.getAsFile()) + .filter((file) => file); + + debug('dropHandler', files); + uploadFiles(files, uploadInputs[0].getAttribute('data-route')); + }; + + const dropWindowHandler = (ev) => { + if ([...ev.dataTransfer.items].some((item) => item.kind === 'file')) { + ev.preventDefault(); + } + }; + + const editClickHandler = () => { + if (isEditing) return; + + if (twtxtEditFormText) twtxtEditFormText.value = fileText; + + beginEditMode(); + }; + + const editResetHandler = (ev) => { + ev.preventDefault(); + + if (!isEditing && !confirm('Do you want to quit editing?')) return; + + if (fileText && fileBox) { + fileBox.textContent = fileText; + } + + endEditMode(); + }; + + const editSubmitHandler = async (ev, secondAttempt = false) => { + ev?.preventDefault(); + + try { + const newFileText = twtxtEditFormText.value; + + debug('edit file submit', { newFileText }); + if (!newFileText) return; + + const res = await fetch(`${TWTXT_FILE_URL}`, { + method: 'PUT', + body: new URLSearchParams({ + fileContents: newFileText, + }), + headers: { + Authorization: `Bearer ${token}`, + }, + credentials: 'include', + }); + + if (!res.ok && !secondAttempt) { + await refreshToken(); + return editSubmitHandler(ev, true); + } + + showToast('File updated'); + + if (newFileText && fileBox) { + fileText = newFileText; + fileBox.textContent = newFileText; + } + + endEditMode(); + + await loadTwtxtFile(); + } catch (err) { + if (!secondAttempt) { + await refreshToken(); + return twtFormSubmitHandler(ev, true); + } + + showToast('Unable to update file, please try again later.', 'error'); + console.error('Error updating file', err); + } + + return false; + }; + + const loginFormSubmitHandler = async (ev) => { + ev?.preventDefault(); + let response; + + try { + const loginData = new URLSearchParams(new FormData(loginForm)); + debug('loginForm submit', { loginData }); + + response = await fetch(TWTXT_FILE_URL, { + method: 'POST', + body: loginData, + mode: 'same-origin', + credentials: 'include', + }); + + debug('loginForm submit', { response }); + + if (!response.ok) throw new Error(response.statusText); + showToast('Login complete'); + } catch (err) { + showToast('Unable to login, please try again later.', 'error'); + console.error('Error logging in', err); + + return; + } + + if (response?.body) token = await new Response(response.body).text(); + + if (token) document.body.classList.add('js-authorized'); + + if (token && rememberToggle.checked) + setCookie(ACCESS_TOKEN_COOKIE_KEY, token, 7); + + debug('loginForm submit', { cookie, token, response }); + + return false; + }; + + const logoutHandler = async () => { + try { + const res = await fetch(`${TWTXT_FILE_URL}`, { + method: 'POST', + body: new URLSearchParams({ + type: 'logout', + }), + credentials: 'include', + }); + + if (!res.ok) throw new Error(); + + document.cookie = `${ACCESS_TOKEN_COOKIE_KEY}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; + + if (menuCheckbox) menuCheckbox.checked = false; + + window.location.reload(); + } catch { + showToast('Unable to logout, please try again later.', 'error'); + } + }; + + const rememberToggleHandler = (ev) => { + if (ev.target.checked) { + localStorage.setItem(REMEMBER_LOGIN_STORAGE_KEY, 'true'); + return; + } + + localStorage.removeItem(REMEMBER_LOGIN_STORAGE_KEY); + }; + + const twtContentKeyupHandler = (ev) => { + const hasContent = + (twtControlsContentInput.value?.trim() ?? '').length !== 0; + + if (twtControlsContentInput) { + requestAnimationFrame(() => { + // Sync with browser repaint + twtControlsContentInput.style.height = 'auto'; + twtControlsContentInput.style.height = `${Math.max(twtControlsContentInput.scrollHeight, 80)}px`; + }); + } + + if (isEditing || !hasContent) + twtSubmitButton.setAttribute('disabled', 'disabled'); + else twtSubmitButton.removeAttribute('disabled'); + + if (hasContent && ev?.key === 'Enter' && ev?.ctrlKey) + twtFormSubmitHandler(); + }; + + const twtFormSubmitHandler = async (ev, secondAttempt = false) => { + ev?.preventDefault(); + + try { + const twtData = new FormData(twtForm); + const twtContent = twtData.get('content').trim(); + + debug('twtForm submit data', { twtData }); + if (!twtContent) return; + + twtData.set('content', twtContent.replaceAll('\n', '\u2028')); + const twtBody = new URLSearchParams(twtData); + debug('twtForm submit body', { twtBody }); + + const res = await fetch(TWTXT_FILE_URL, { + method: 'POST', + body: twtBody, + headers: { + Authorization: `Bearer ${token}`, + }, + credentials: 'include', + }); + + if (!res.ok && !secondAttempt) { + debug('twtForm submit - not OK, trying refresh'); + await refreshToken(); + return twtFormSubmitHandler(ev, true); + } + + showToast('Twt sent'); + + twtForm.reset(); + await loadTwtxtFile(); + + // scroll to bottom of file to show update + fileContentsSection.scrollTop = fileBox.scrollHeight; + } catch (err) { + debug('twtForm submit - error, trying refresh', err, !secondAttempt); + if (!secondAttempt) { + await refreshToken(); + return twtFormSubmitHandler(ev, true); + } + + showToast('Unable to twt, please try again later.', 'error'); + console.error('Error POSTing twt', err); + } + return false; + }; + + const uploadChangeHandler = (ev) => { + uploadFiles(ev.target.files, ev.target.getAttribute('data-route')); + }; + + /* Attach Handlers to Listeners */ + + Array.from(uploadInputs).forEach((uploadInput) => { + uploadInput.addEventListener('change', uploadChangeHandler); + }); + + loginForm.addEventListener('submit', loginFormSubmitHandler); + + twtForm.addEventListener('submit', twtFormSubmitHandler); + + twtForm.addEventListener('keyup', twtContentKeyupHandler); + + twtControlsContentInput.addEventListener('drop', dropHandler); + + twtControlsContentInput.addEventListener('dragover', dragOverHandler); + + twtLogoutButton.addEventListener('click', logoutHandler); + + twtFileEditButton.addEventListener('click', editClickHandler); + + twtxtEditForm.addEventListener('reset', editResetHandler); + + twtxtEditForm.addEventListener('submit', editSubmitHandler); + + window.addEventListener('dragover', dragOverWindowHandler); + + window.addEventListener('drop', dropWindowHandler); + + rememberToggle.addEventListener('change', rememberToggleHandler); + + /* Start App*/ + + loadTwtxtFile().catch(() => {}); + + token = getCookie(ACCESS_TOKEN_COOKIE_KEY); + if (token) document.body.classList.add('js-authorized'); + + debug('client loaded'); +})(); diff --git a/src/client/styles.css b/src/client/styles.css new file mode 100644 index 0000000..1a976e8 --- /dev/null +++ b/src/client/styles.css @@ -0,0 +1,717 @@ +/** + * Taken from Normalize.css v12.1.1 / https://csstools.github.io/normalize.css/ + */ +:where(html) { + line-height: 1.15; + -webkit-text-size-adjust: 100%; + text-size-adjust: 100%; +} +:where(b, strong) { + font-weight: bolder; +} +:where(code, kbd, pre, samp) { + font-family: monospace, monospace; + font-size: 1em; +} +:where(button, input, select) { + margin: 0; +} +:where(button) { + text-transform: none; +} +:where( + button, + input:is([type='button' i], [type='reset' i], [type='submit' i]) +) { + -webkit-appearance: button; + appearance: button; +} +:where(select) { + text-transform: none; +} +:where(textarea) { + margin: 0; +} +:where(input[type='search' i]) { + -webkit-appearance: textfield; + appearance: textfield; + outline-offset: -2px; +} +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} +::-webkit-input-placeholder { + color: inherit; + opacity: 0.54; +} +::-webkit-search-decoration { + -webkit-appearance: none; +} +::-webkit-file-upload-button { + -webkit-appearance: button; + font: inherit; +} +:where( + button, + input:is( + [type='button' i], + [type='color' i], + [type='reset' i], + [type='submit' i] + ) +)::-moz-focus-inner { + border-style: none; + padding: 0; +} +:where( + button, + input:is( + [type='button' i], + [type='color' i], + [type='reset' i], + [type='submit' i] + ) +)::-moz-focusring { + outline: 1px dotted ButtonText; +} + +/** + * Local styles + */ +:root { + /* #0a0a14 / Vulcan*/ + --bg: rgb(10, 10, 20); + --bg-al: rgb(10, 10, 20, 0.6); + + /* #1b1b27 / Steel Gray */ + --bg-hl: rgb(27, 27, 39); + + /* #6e6e81 / Storm Gray */ + --fg: rgb(110, 110, 129); + + --fg-hl: #ccc; + + /* #9f9fc1 / Logan */ + --link: rgb(159, 159, 193); +} + +* { + box-sizing: border-box; +} + +body { + background-color: var(--bg); + color: var(--fg); +} + +a { + border-radius: 0.5rem; + color: var(--link); + padding: 0 0.25rem; + transition: all 0.5s; +} + +a:hover { + color: var(--fg-hl); + background-color: var(--bg-hl); +} + +h1, +h2, +h3, +h4, +h5, +h6 { + color: var(--fg-hl); +} + +input, +textarea { + background-color: var(--bg-hl); + border-color: var(--fg); + color: var(--fg); +} + +button, +input[type='reset'], +input[type='submit'] { + background-color: var(--bg-hl); + border-color: var(--link); + color: var(--link); +} + +button:disabled, +input:disabled { + opacity: 0.2; +} + +.appInfo { + font-size: smaller; + font-style: italic; + margin: 0.5rem auto; + text-align: center; +} + +.button { + background-color: var(--bg-hl); + border: 1px solid var(--link); + border-radius: 0.5rem; + color: var(--link); + padding: 0.5rem; +} + +.fileContentsSection { + margin-bottom: 7rem; + max-width: 100vw; + overflow: auto; +} + +.fileContentsSection-fileBox { + margin: 0; + max-height: 1000rem; + overflow-y: hidden; + transition: all 0.5s; +} + +.hamburgerToggle { + grid-area: menuButton; + max-width: 3rem; +} + +.hamburgerToggle-icon, +.hamburgerToggle-icon:after, +.hamburgerToggle-icon:before { + background-color: var(--fg); + height: 0.25rem; + position: absolute; + transition-duration: 0.5s; + width: 2rem; +} + +.hamburgerToggle-icon { + top: 12px; +} + +.hamburgerToggle-icon:before { + content: ''; + top: -12px; +} + +.hamburgerToggle-icon:after { + content: ''; + top: 12px; +} + +.hamburgerToggle-label { + cursor: pointer; + display: block; + height: 1.75rem; + left: 0; + position: relative; + top: 0; + transition-duration: 0.5s; + width: 2rem; +} + +#hamburgerToggleCheckbox { + opacity: 0; + cursor: pointer; + position: absolute; +} + +#hamburgerToggleCheckbox:checked + + .hamburgerToggle-label + .hamburgerToggle-icon { + transition-duration: 0.5s; + background: transparent; +} + +#hamburgerToggleCheckbox:checked + + .hamburgerToggle-label + .hamburgerToggle-icon:before { + transform: rotateZ(45deg) scaleX(1.25) translate(8px, 8px); +} + +#hamburgerToggleCheckbox:checked + + .hamburgerToggle-label + .hamburgerToggle-icon:after { + transform: rotateZ(-45deg) scaleX(1.25) translate(8px, -8px); +} + +#hamburgerToggleCheckbox:checked ~ .popupMenu { + right: 0.5rem; +} + +.loginControls { + max-height: 100rem; + overflow: hidden; +} + +.loginControls-fields { + display: flex; + flex-direction: column; + gap: 0.5rem; + justify-content: end; +} + +.loginControls-fields-row { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 0.5rem; + justify-content: end; +} + +.loginControls-input { + max-width: 8rem; +} + +.loginControls input:focus { + border: 1px solid #eee; +} + +.loginControls-label { + margin-right: 1rem; +} + +.loginControls-row { + align-items: center; + display: flex; + flex-direction: row; + justify-content: end; +} + +.loginControls-toggle { + align-items: center; + border-radius: 8rm; + display: flex; + justify-content: end; + margin-right: 1rem; +} + +.loginControls-toggle-checkbox { + clip: rect(0 0 0 0); + clip-path: inset(50%); + height: 1px; + overflow: hidden; + position: absolute; + white-space: nowrap; + width: 1px; +} + +.loginControls-toggle-checkbox:not([disabled]):active + + .loginControls-toggle-track, +.loginControls-toggle-checkbox:not([disabled]):focus + + .loginControls-toggle-track { + /* border: 1px solid transparent; */ + box-shadow: 0px 0px 0px 2px #333; +} +.loginControls-toggle-checkbox:disabled + .loginControls-toggle-track { + cursor: not-allowed; + opacity: 0.7; +} + +.loginControls-toggle-track { + background: var(--bg); + border: 1px solid var(--fg); + border-radius: 100px; + cursor: pointer; + display: flex; + height: 1.5rem; + margin-left: 0.5rem; + position: relative; + width: 3rem; +} + +.loginControls-toggle-indicator { + align-items: center; + background: var(--bg-hl); + border: 1px solid var(--fg); + border-radius: 1rem; + bottom: 0.1rem; + display: flex; + height: 1.25rem; + justify-content: center; + left: 0.1rem; + outline: solid 2px transparent; + position: absolute; + transition: 0.25s; + width: 1.25rem; +} + +.loginControls-toggle-checkMark { + fill: var(--fg); + height: 1rem; + width: 1rem; + opacity: 0; + transition: opacity 0.25s ease-in-out; +} + +.loginControls-toggle-checkbox:checked + + .loginControls-toggle-track + .loginControls-toggle-indicator { + transform: translateX(1.4rem); +} +.loginControls-toggle-checkbox:checked + + .loginControls-toggle-track + .loginControls-toggle-indicator + .loginControls-toggle-checkMark { + opacity: 1; + transition: opacity 0.25s ease-in-out; +} + +.loginControls-submitButton { + margin-left: 0.5rem; +} + +.menu { + background-color: var(--bg-al); + backdrop-filter: blur(5px) saturate(70%); + left: 0; + padding: 0.5rem; + position: fixed; + bottom: 0; + width: 100%; +} + +.popupMenu { + background-color: var(--bg-hl); + border: 1px solid var(--fg); + border-radius: 0.5rem; + display: flex; + flex-direction: column; + justify-content: space-around; + right: -10rem; + padding: 0.5rem; + position: fixed; + top: -14rem; + transition: all 0.5s; + width: 5rem; +} + +.popupMenu > * { + margin-bottom: 0.5rem; +} + +.popupMenu > *:last-child { + margin-bottom: 0; +} + +.popupMenu-appInfo { + margin-top: 0; +} + +.toast { + display: flex; + align-items: center; + padding: 10px 20px; + background-color: #4caf50; + color: #ffffff; + border-radius: 5px; + font-size: 16px; + animation: + slideIn 0.5s, + fadeOut 0.5s 3s; + opacity: 1; +} + +.toast.error { + background-color: #f44336; +} + +.toastContainer { + position: fixed; + bottom: 8rem; + right: 2rem; + display: flex; + flex-direction: column; + gap: 1rem; + z-index: 1000; +} + +.twtControls { + align-items: center; + display: flex; + flex-direction: row; + max-height: 0; + overflow: hidden; + position: relative; +} + +.twtControls-appAuthor { + display: none; +} + +.twtControls-appInfo { + display: none; +} + +.twtControls-contentInput { + width: 100%; + min-height: 5rem; + overflow-y: auto; + resize: none; + transition: max-height 0.2s ease; +} + +.twtControls-contentLabel { + align-items: center; + display: flex; + flex-direction: row; + grid-area: textarea; + width: 100%; +} + +.twtControls-form { + width: 100%; +} + +.twtControls-formRow { + align-items: center; + display: grid; + grid-gap: 0.5rem; + grid-template-areas: + 'textarea menuButton' + 'textarea postButton'; + grid-template-columns: 1fr 3rem; + grid-template-rows: auto; + justify-content: space-between; +} + +.twtControls-gitLink { + display: none; +} + +.twtControls-uploadInputLabel { + align-items: center; + cursor: pointer; + display: flex; + justify-content: center; + max-width: 100%; + text-align: center; + transition: all 0.5s; +} + +.twtControls-uploadInputLabel-normal { + display: none; +} + +.twtControls-uploadInputLabel-small { + font-size: small; +} + +.twtControls-uploadInputLabel:hover { + background-color: var(--bg-hl); + color: var(--fg-hl); +} + +.twtControls-uploadInput { + display: none; +} + +.twtControls-submitButton { + background-color: var(--bg-hl); + border: 1px solid var(--link); + border-radius: 0.5rem; + color: var(--link); + grid-area: postButton; + height: 3rem; + width: 3rem; +} + +.twtxtEditForm { + max-height: 0; + overflow: hidden; + transition: all 0.5s; + width: 100%; +} + +.twtxtEditForm-controls { + display: flex; + gap: 0.5rem; + justify-content: end; + margin-top: 1rem; +} + +.twtxtEditForm-textarea { + font-size: large; + height: 80vh; + min-height: 6rem; + white-space: nowrap; + width: 100%; +} + +/* Media-based Overrides */ + +@media (min-width: 24rem) { + .loginControls-input { + max-width: 10rem; + } +} + +@media (min-width: 28rem) { + .loginControls-input { + max-width: 12rem; + } +} + +@media (min-width: 600px) { + #hamburgerToggleCheckbox:checked ~ .popupMenu { + left: 0.5rem; + } + + .popupMenu { + left: -10rem; + top: -6.5rem; + } + + .twtControls { + flex-direction: row; + } + + .twtControls-appInfo { + display: flex; + flex-direction: column; + grid-area: appInfo; + } + + .twtControls-submitButton { + justify-self: end; + } + + .twtControls-formRow { + grid-template-areas: 'menuButton appInfo textarea postButton'; + grid-template-columns: 3.5rem 3.5rem 1fr 3.5rem; + grid-template-rows: auto; + } +} + +@media (min-width: 900px) { + .popupMenu-appInfo { + display: none; + } + + .twtControls-appAuthor { + display: inline-block; + max-width: 20rem; + } + + .twtControls-appInfo { + display: block; + grid-area: appInfo; + } + + .twtControls-contentInput { + min-height: 5rem; + } + + .twtControls-formRow { + grid-template-areas: 'menuButton appInfo uploadButton textarea postButton'; + grid-template-columns: 3.5rem 1fr 5rem 1fr 3.5rem; + grid-template-rows: auto; + } + + .twtControls-gitLink { + display: block; + } + + .twtControls-uploadInputLabel-normal { + display: block; + grid-area: uploadButton; + margin-right: 0.5rem; + } + + .twtControls-uploadInputLabel-small { + display: none; + } +} + +@media (min-width: 1200px) { + .fileContentsSection { + margin-bottom: 1rem; + margin-top: 6rem; + } + + .menu { + top: 0; + bottom: auto; + } + + .popupMenu { + left: -10rem; + top: 5rem; + } + + .toastContainer { + bottom: 2rem; + } +} + +@media screen and (-ms-high-contrast: active) { + .loginControls-toggle-track { + border-radius: 0; + } +} + +/* State-Based Overrides */ + +.js-authorized .loginControls { + max-height: 0; +} + +.js-authorized .twtControls { + max-height: 100rem; +} + +.js-editMode .fileContentsSection-fileBox { + max-height: 0; + overflow: hidden; + transition: all 0.5s; +} + +.js-editMode .fileContentsSection-twtxtEditForm { + max-height: 100rem; +} + +/* TODO: Fix */ +@media (prefers-color-scheme: dark) { + :root { + /* #6e6e81 / Storm Gray */ + --bg: #ccc; + + --bg-al: rgb(204, 204, 204, 0.6); + + --bg-hl: #b6b6c0; + + /* #0a0a14 / Vulcan*/ + --fg: rgb(10, 10, 20); + + /* #1b1b27 / Steel Gray */ + --fg-hl: rgb(27, 27, 39); + + --link: #35353e; + } +} + +/* Animations */ + +@keyframes slideIn { + from { + transform: translateX(100%); + } + + to { + transform: translateX(0); + } +} + +@keyframes fadeOut { + from { + opacity: 1; + } + + to { + opacity: 0; + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..036faae --- /dev/null +++ b/src/index.ts @@ -0,0 +1 @@ +export { default } from "./plugin.js"; diff --git a/src/lib/arrayDB.ts b/src/lib/arrayDB.ts new file mode 100644 index 0000000..fbf8966 --- /dev/null +++ b/src/lib/arrayDB.ts @@ -0,0 +1,89 @@ +import Debug from 'debug'; +import { join } from 'node:path'; + +import { loadObjectFromJson, saveToJson } from './utils.js'; + +const debug = Debug('twtkpr:arrayDB'); + +/** + * File-backed, in-memory database consisting of arrays of strings (ex: tokens) indexed by other + * strings (ex: usernames). + * + * @param name + * @param directory + * @returns + */ +export default async function arrayDB(name: string, directory: string) { + let theName: string; + let dataObject: Record; + + const get = (key = '') => { + debug('get', { key }); + + if (!theName || !dataObject) + throw new Error('DB must be initialized first'); + + key = key?.trim(); + if (!key) throw new Error('a valid key must be provided'); + + return dataObject[key]; + }; + + const getObject = () => dataObject; + + const initialize = async (dbName = '') => { + debug('initialize starting', { dbName }); + + dbName = dbName?.trim(); + if (!dbName) throw new Error('a valid name must be provided'); + + try { + dataObject = await loadObjectFromJson(join(directory, `${dbName}.json`)); + } catch (err: unknown) { + debug('initialize read error', { err }); + + if ((err as { code: string }).code === 'ENOENT') dataObject = {}; + else throw err; + } + + // only initialize (and set name) if everything passes + theName = dbName; + debug('initialize complete', { dataObject, name: theName }); + }; + + const remove = (key = '') => { + debug('remove', { key }); + + if (!theName || !dataObject) + throw new Error('DB must be initialized first'); + + key = key?.trim(); + if (!key) throw new Error('a valid key must be provided'); + + delete dataObject[key]; + }; + + const set = (key = '', value: string[] = []) => { + debug('set', { key }); + + if (!theName || !dataObject) + throw new Error('DB must be initialized first'); + + key = key?.trim(); + if (!key) throw new Error('a valid key must be provided'); + + dataObject[key] = value; + saveToJson(dataObject, join(directory, `${name}.json`)); + + return value; + }; + + await initialize(name); + + return { + get, + getObject, + remove, + set, + }; +} diff --git a/src/lib/constants.ts b/src/lib/constants.ts new file mode 100644 index 0000000..d4c856f --- /dev/null +++ b/src/lib/constants.ts @@ -0,0 +1,29 @@ +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +export const DEFAULT_PRIVATE_DIRECTORY = '.data'; +export const DEFAULT_PUBLIC_DIRECTORY = 'public'; +export const DEFAULT_TWTXT_FILENAME = 'twtxt.txt'; +export const DEFAULT_ROUTE = `/${DEFAULT_TWTXT_FILENAME}`; + +export const DEFAULT_POST_LIMITER_ACTIVE = true; + +export const DEFAULT_QUERY_PARAMETER_APP = 'app'; +export const DEFAULT_QUERY_PARAMETER_CSS = 'css'; +export const DEFAULT_QUERY_PARAMETER_FOLLOWING = 'following'; +export const DEFAULT_QUERY_PARAMETER_JS = 'js'; +export const DEFAULT_QUERY_PARAMETER_LOGOUT = 'logout'; +export const DEFAULT_QUERY_PARAMETER_METADATA = 'metadata'; +export const DEFAULT_QUERY_PARAMETER_TWT = 'twt'; +export const DEFAULT_QUERY_PARAMETER_TWTS = 'twts'; + +export const DEFAULT_UPLOAD_ACTIVE = true; +export const DEFAULT_UPLOAD_ALLOWED_MIME_TYPES = ''; +export const DEFAULT_UPLOAD_ROUTE = 'files'; +export const DEFAULT_UPLOAD_ENCODING = 'utf-8'; + +// optional in zod +export const DEFAULT_UPLOAD_HASH_ALGORITHM = 'sha256'; +export const DEFAULT_UPLOAD_KEEP_EXTENSIONS = true; + +export const __dirname = resolve(dirname(fileURLToPath(import.meta.url)), '..'); diff --git a/src/lib/env.ts b/src/lib/env.ts new file mode 100644 index 0000000..f969448 --- /dev/null +++ b/src/lib/env.ts @@ -0,0 +1,307 @@ +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { z } from 'zod/v4'; + +import { + DEFAULT_POST_LIMITER_ACTIVE, + DEFAULT_PRIVATE_DIRECTORY, + DEFAULT_PUBLIC_DIRECTORY, + DEFAULT_QUERY_PARAMETER_APP, + DEFAULT_QUERY_PARAMETER_CSS, + DEFAULT_QUERY_PARAMETER_FOLLOWING, + DEFAULT_QUERY_PARAMETER_JS, + DEFAULT_QUERY_PARAMETER_LOGOUT, + DEFAULT_QUERY_PARAMETER_METADATA, + DEFAULT_QUERY_PARAMETER_TWT, + DEFAULT_QUERY_PARAMETER_TWTS, + DEFAULT_ROUTE, + DEFAULT_TWTXT_FILENAME, + DEFAULT_UPLOAD_ACTIVE, + DEFAULT_UPLOAD_ALLOWED_MIME_TYPES, + DEFAULT_UPLOAD_ENCODING, + DEFAULT_UPLOAD_HASH_ALGORITHM, + DEFAULT_UPLOAD_KEEP_EXTENSIONS, + DEFAULT_UPLOAD_ROUTE, +} from './constants.js'; + +/* + The following keys are expected to exist in `process.env`, either as listed, or without the + `TWTKPR_` prefix + + We only have listed default values for our keys, anything for other plugins (like formidable or + express-rate-limit) fall back to their own defaults (and thus are optional). +*/ +const envSchema = z.object({ + NODE_ENV: z + .enum(['development', 'production', 'test']) + .default('development'), + + // required vars - MUST be passed via ENV + TWTKPR_REFRESH_SECRET: z.string().default(''), + TWTKPR_ACCESS_SECRET: z.string().default(''), + + // vars with default values + TWTKPR_DEFAULT_ROUTE: z.string().default(DEFAULT_ROUTE), + TWTKPR_PRIVATE_DIRECTORY: z.string().default(DEFAULT_PRIVATE_DIRECTORY), + TWTKPR_PUBLIC_DIRECTORY: z.string().default(DEFAULT_PUBLIC_DIRECTORY), + TWTKPR_QUERY_PARAMETER_APP: z.string().default(DEFAULT_QUERY_PARAMETER_APP), + TWTKPR_QUERY_PARAMETER_CSS: z.string().default(DEFAULT_QUERY_PARAMETER_CSS), + TWTKPR_QUERY_PARAMETER_FOLLOWING: z + .string() + .default(DEFAULT_QUERY_PARAMETER_FOLLOWING), + TWTKPR_QUERY_PARAMETER_JS: z.string().default(DEFAULT_QUERY_PARAMETER_JS), + TWTKPR_QUERY_PARAMETER_LOGOUT: z + .string() + .default(DEFAULT_QUERY_PARAMETER_LOGOUT), + TWTKPR_QUERY_PARAMETER_METADATA: z + .string() + .default(DEFAULT_QUERY_PARAMETER_METADATA), + TWTKPR_QUERY_PARAMETER_TWT: z.string().default(DEFAULT_QUERY_PARAMETER_TWT), + TWTKPR_QUERY_PARAMETER_TWTS: z.string().default(DEFAULT_QUERY_PARAMETER_TWTS), + TWTKPR_TWTXT_FILENAME: z.string().default(DEFAULT_TWTXT_FILENAME), + + /** + * Post limiter plugin + */ + + // var with default value + TWTKPR_POST_LIMITER_ACTIVE: z.boolean().default(DEFAULT_POST_LIMITER_ACTIVE), + + // optional vars + TWTKPR_POST_LIMITER_WINDOW_MS: z.optional(z.number()), + TWTKPR_POST_LIMITER_LIMIT: z.optional(z.union([z.number(), z.function()])), + TWTKPR_POST_LIMITER_MESSAGE: z.optional(z.any()), + TWTKPR_POST_LIMITER_STATUS_CODE: z.optional(z.number()), + TWTKPR_POST_LIMITER_HANDLER: z.optional(z.function()), + TWTKPR_POST_LIMITER_LEGACY_HEADERS: z.optional(z.boolean()), + TWTKPR_POST_LIMITER_STANDARD_HEADERS: z.optional( + z.union([z.boolean(), z.string()]) + ), + TWTKPR_POST_LIMITER_IDENTIFIER: z.optional( + z.union([z.string(), z.function()]) + ), + TWTKPR_POST_LIMITER_STORE: z.optional(z.any()), + TWTKPR_POST_LIMITER_PASS_ON_STORE_ERROR: z.optional(z.boolean()), + TWTKPR_POST_LIMITER_KEY_GENERATOR: z.optional(z.function()), + TWTKPR_POST_LIMITER_IPV6_SUBNET: z.optional( + z.union([z.number(), z.function(), z.boolean()]) + ), + TWTKPR_POST_LIMITER_REQUEST_PROPERTY_NAME: z.optional(z.string()), + TWTKPR_POST_LIMITER_SKIP: z.optional(z.function()), + TWTKPR_POST_LIMITER_SKIP_SUCCESSFUL_REQUESTS: z.optional(z.boolean()), + TWTKPR_POST_LIMITER_SKIP_FAILED_REQUESTS: z.optional(z.boolean()), + TWTKPR_POST_LIMITER_REQUEST_WAS_SUCCESSFUL: z.optional(z.function()), + TWTKPR_POST_LIMITER_VALIDATE: z.optional(z.union([z.boolean(), z.object()])), + + /** + * Upload plugin + */ + + // vars with default values + TWTKPR_UPLOAD_ACTIVE: z.boolean().default(DEFAULT_UPLOAD_ACTIVE), + TWTKPR_UPLOAD_ALLOWED_MIME_TYPES: z + .union([z.string(), z.array(z.string())]) + .default(DEFAULT_UPLOAD_ALLOWED_MIME_TYPES), + TWTKPR_UPLOAD_ROUTE: z.string().default(DEFAULT_UPLOAD_ROUTE), + + // optional vars + TWTKPR_UPLOAD_ALLOW_EMPTY_FILES: z.optional(z.boolean()), + TWTKPR_UPLOAD_CREATE_DIRS_FROM_UPLOADS: z.optional(z.boolean()), + TWTKPR_UPLOAD_DIRECTORY: z.optional(z.string()), + TWTKPR_UPLOAD_ENCODING: z.string().default(DEFAULT_UPLOAD_ENCODING), + TWTKPR_UPLOAD_FILE_WRITE_STREAM_HANDLER: z.optional(z.function()), + TWTKPR_UPLOAD_FILENAME: z.optional(z.function()), + TWTKPR_UPLOAD_FILTER: z.optional(z.function()), + TWTKPR_UPLOAD_HASH_ALGORITHM: z + .union([z.boolean(), z.string()]) + .default(DEFAULT_UPLOAD_HASH_ALGORITHM), + TWTKPR_UPLOAD_KEEP_EXTENSIONS: z + .boolean() + .default(DEFAULT_UPLOAD_KEEP_EXTENSIONS), + TWTKPR_UPLOAD_MAX_FIELDS: z.optional(z.number()), + TWTKPR_UPLOAD_MAX_FIELDS_SIZE: z.optional(z.number()), + TWTKPR_UPLOAD_MAX_FILE_SIZE: z.optional(z.number()), + TWTKPR_UPLOAD_MAX_FILES: z.optional(z.number()), + TWTKPR_UPLOAD_MAX_TOTAL_FILE_SIZE: z.optional(z.number()), + TWTKPR_UPLOAD_MIN_FILE_SIZE: z.optional(z.number()), +}); + +const parseEnv = () => { + try { + /** + * there's probably an easier way to do this, spreading the keys from above and accepting either + * the app (bare) or library (`TWTKPR_`-prefixed) version of said key. + * But this should work for now. + */ + const parsedEnv = envSchema.parse({ + NODE_ENV: process.env.TWTKPR_NODE_ENV || process.env.NODE_ENV, + TWTKPR_ACCESS_SECRET: + process.env.TWTKPR_ACCESS_SECRET || process.env.ACCESS_SECRET, + TWTKPR_DEFAULT_ROUTE: + process.env.TWTKPR_DEFAULT_ROUTE || process.env.DEFAULT_ROUTE, + TWTKPR_PRIVATE_DIRECTORY: + process.env.TWTKPR_PRIVATE_DIRECTORY || process.env.PRIVATE_DIRECTORY, + TWTKPR_PUBLIC_DIRECTORY: + process.env.TWTKPR_PUBLIC_DIRECTORY || process.env.PUBLIC_DIRECTORY, + TWTKPR_REFRESH_SECRET: + process.env.TWTKPR_REFRESH_SECRET || process.env.REFRESH_SECRET, + TWTKPR_TWTXT_FILENAME: + process.env.TWTKPR_TWTXT_FILENAME || process.env.TWTXT_FILENAME, + + TWTKPR_POST_LIMITER_ACTIVE: + process.env.TWTKPR_POST_LIMITER_ACTIVE || + process.env.POST_LIMITER_ACTIVE, + TWTKPR_POST_LIMITER_WINDOW_MS: + process.env.TWTKPR_POST_LIMITER_WINDOW_MS || + process.env.POST_LIMITER_WINDOW_MS, + TWTKPR_POST_LIMITER_LIMIT: + process.env.TWTKPR_POST_LIMITER_LIMIT || process.env.POST_LIMITER_LIMIT, + TWTKPR_POST_LIMITER_MESSAGE: + process.env.TWTKPR_POST_LIMITER_MESSAGE || + process.env.POST_LIMITER_MESSAGE, + TWTKPR_POST_LIMITER_STATUS_CODE: + process.env.TWTKPR_POST_LIMITER_STATUS_CODE || + process.env.POST_LIMITER_STATUS_CODE, + TWTKPR_POST_LIMITER_HANDLER: + process.env.TWTKPR_POST_LIMITER_HANDLER || + process.env.POST_LIMITER_HANDLER, + TWTKPR_POST_LIMITER_LEGACY_HEADERS: + process.env.TWTKPR_POST_LIMITER_LEGACY_HEADERS || + process.env.POST_LIMITER_LEGACY_HEADERS, + TWTKPR_POST_LIMITER_STANDARD_HEADERS: + process.env.TWTKPR_POST_LIMITER_STANDARD_HEADERS || + process.env.POST_LIMITER_STANDARD_HEADERS, + TWTKPR_POST_LIMITER_IDENTIFIER: + process.env.TWTKPR_POST_LIMITER_IDENTIFIER || + process.env.POST_LIMITER_IDENTIFIER, + TWTKPR_POST_LIMITER_STORE: + process.env.TWTKPR_POST_LIMITER_STORE || process.env.POST_LIMITER_STORE, + TWTKPR_POST_LIMITER_PASS_ON_STORE_ERROR: + process.env.TWTKPR_POST_LIMITER_PASS_ON_STORE_ERROR || + process.env.POST_LIMITER_PASS_ON_STORE_ERROR, + TWTKPR_POST_LIMITER_KEY_GENERATOR: + process.env.TWTKPR_POST_LIMITER_KEY_GENERATOR || + process.env.POST_LIMITER_KEY_GENERATOR, + TWTKPR_POST_LIMITER_IPV6_SUBNET: + process.env.TWTKPR_POST_LIMITER_IPV6_SUBNET || + process.env.POST_LIMITER_IPV6_SUBNET, + TWTKPR_POST_LIMITER_REQUEST_PROPERTY_NAME: + process.env.TWTKPR_POST_LIMITER_REQUEST_PROPERTY_NAME || + process.env.POST_LIMITER_REQUEST_PROPERTY_NAME, + TWTKPR_POST_LIMITER_SKIP: + process.env.TWTKPR_POST_LIMITER_SKIP || process.env.POST_LIMITER_SKIP, + TWTKPR_POST_LIMITER_SKIP_SUCCESSFUL_REQUESTS: + process.env.TWTKPR_POST_LIMITER_SKIP_SUCCESSFUL_REQUESTS || + process.env.POST_LIMITER_SKIP_SUCCESSFUL_REQUESTS, + TWTKPR_POST_LIMITER_SKIP_FAILED_REQUESTS: + process.env.TWTKPR_POST_LIMITER_SKIP_FAILED_REQUESTS || + process.env.POST_LIMITER_SKIP_FAILED_REQUESTS, + TWTKPR_POST_LIMITER_REQUEST_WAS_SUCCESSFUL: + process.env.TWTKPR_POST_LIMITER_REQUEST_WAS_SUCCESSFUL || + process.env.POST_LIMITER_REQUEST_WAS_SUCCESSFUL, + TWTKPR_POST_LIMITER_VALIDATE: + process.env.TWTKPR_POST_LIMITER_VALIDATE || + process.env.POST_LIMITER_VALIDATE, + + TWTKPR_QUERY_PARAMETER_APP: + process.env.TWTKPR_QUERY_PARAMETER_APP || + process.env.QUERY_PARAMETER_APP, + TWTKPR_QUERY_PARAMETER_CSS: + process.env.TWTKPR_QUERY_PARAMETER_CSS || + process.env.QUERY_PARAMETER_CSS, + TWTKPR_QUERY_PARAMETER_FOLLOWING: + process.env.TWTKPR_QUERY_PARAMETER_FOLLOWING || + process.env.QUERY_PARAMETER_FOLLOWING, + TWTKPR_QUERY_PARAMETER_JS: + process.env.TWTKPR_QUERY_PARAMETER_JS || process.env.QUERY_PARAMETER_JS, + TWTKPR_QUERY_PARAMETER_LOGOUT: + process.env.TWTKPR_QUERY_PARAMETER_LOGOUT || + process.env.QUERY_PARAMETER_LOGOUT, + TWTKPR_QUERY_PARAMETER_METADATA: + process.env.TWTKPR_QUERY_PARAMETER_METADATA || + process.env.QUERY_PARAMETER_METADATA, + TWTKPR_QUERY_PARAMETER_TWT: + process.env.TWTKPR_QUERY_PARAMETER_TWT || + process.env.QUERY_PARAMETER_TWT, + TWTKPR_QUERY_PARAMETER_TWTS: + process.env.TWTKPR_QUERY_PARAMETER_TWTS || + process.env.QUERY_PARAMETER_TWTS, + + TWTKPR_UPLOAD_ACTIVE: + process.env.TWTKPR_UPLOAD_ACTIVE || process.env.UPLOAD_ACTIVE, + TWTKPR_UPLOAD_ALLOW_EMPTY_FILES: + process.env.TWTKPR_UPLOAD_ALLOW_EMPTY_FILES || + process.env.UPLOAD_ALLOW_EMPTY_FILES, + TWTKPR_UPLOAD_ALLOWED_MIME_TYPES: + process.env.TWTKPR_UPLOAD_ALLOWED_MIME_TYPES || + process.env.UPLOAD_ALLOWED_MIME_TYPES, + TWTKPR_UPLOAD_CREATE_DIRS_FROM_UPLOADS: + process.env.TWTKPR_UPLOAD_CREATE_DIRS_FROM_UPLOADS || + process.env.UPLOAD_CREATE_DIRS_FROM_UPLOADS, + TWTKPR_UPLOAD_DIRECTORY: + process.env.TWTKPR_UPLOAD_DIRECTORY || + process.env.UPLOAD_DIRECTORY || + process.env.UPLOAD_DIR, + TWTKPR_UPLOAD_ENCODING: + process.env.TWTKPR_UPLOAD_ENCODING || process.env.UPLOAD_ENCODING, + TWTKPR_UPLOAD_FILE_WRITE_STREAM_HANDLER: + process.env.TWTKPR_UPLOAD_FILE_WRITE_STREAM_HANDLER || + process.env.UPLOAD_FILE_WRITE_STREAM_HANDLER, + TWTKPR_UPLOAD_FILENAME: + process.env.TWTKPR_UPLOAD_FILENAME || process.env.UPLOAD_FILENAME, + TWTKPR_UPLOAD_FILTER: + process.env.TWTKPR_UPLOAD_FILTER || process.env.UPLOAD_FILTER, + TWTKPR_UPLOAD_HASH_ALGORITHM: + process.env.TWTKPR_UPLOAD_HASH_ALGORITHM || + process.env.UPLOAD_HASH_ALGORITHM, + TWTKPR_UPLOAD_KEEP_EXTENSIONS: + process.env.TWTKPR_UPLOAD_KEEP_EXTENSIONS || + process.env.UPLOAD_KEEP_EXTENSIONS, + TWTKPR_UPLOAD_MAX_FIELDS: + process.env.TWTKPR_UPLOAD_MAX_FIELDS || process.env.UPLOAD_MAX_FIELDS, + TWTKPR_UPLOAD_MAX_FIELDS_SIZE: + process.env.TWTKPR_UPLOAD_MAX_FIELDS_SIZE || + process.env.UPLOAD_MAX_FIELDS_SIZE, + TWTKPR_UPLOAD_MAX_FILE_SIZE: + process.env.TWTKPR_UPLOAD_MAX_FILE_SIZE || + process.env.UPLOAD_MAX_FILE_SIZE, + TWTKPR_UPLOAD_MAX_FILES: + process.env.TWTKPR_UPLOAD_MAX_FILES || process.env.UPLOAD_MAX_FILES, + TWTKPR_UPLOAD_MAX_TOTAL_FILE_SIZE: + process.env.TWTKPR_UPLOAD_MAX_TOTAL_FILE_SIZE || + process.env.UPLOAD_MAX_TOTAL_FILE_SIZE, + TWTKPR_UPLOAD_MIN_FILE_SIZE: + process.env.TWTKPR_UPLOAD_MIN_FILE_SIZE || + process.env.UPLOAD_MIN_FILE_SIZE, + TWTKPR_UPLOAD_ROUTE: + process.env.TWTKPR_UPLOAD_ROUTE || process.env.UPLOAD_ROUTE, + }); + + if (!parsedEnv.TWTKPR_ACCESS_SECRET) + throw new Error( + 'Either ACCESS_SECRET or TWTKPR_ACCESS_SECRET must be provided' + ); + + if (!parsedEnv.TWTKPR_REFRESH_SECRET) + throw new Error( + 'Either REFRESH_SECRET or TWTKPR_REFRESH_SECRET must be provided' + ); + + return parsedEnv; + } catch (error) { + if (error instanceof z.ZodError) { + console.error( + 'Missing environment variables:', + error.issues.flatMap((issue) => `${issue.path} or TWTKPR_${issue.path}`) + ); + } else { + console.error(error); + } + + process.exit(1); + } +}; + +export const env = parseEnv(); + +export const __dirname = resolve(dirname(fileURLToPath(import.meta.url)), '..'); diff --git a/src/lib/getConfiguration.ts b/src/lib/getConfiguration.ts new file mode 100644 index 0000000..eb0c1e1 --- /dev/null +++ b/src/lib/getConfiguration.ts @@ -0,0 +1,161 @@ +import { + MimeOptions, + TwtKprConfiguration, + TwtKprPluginConfiguration, +} from '../types.js'; +import { env } from './env.js'; + +/** + * + * @param allowedMimeTypes + * @returns + */ +const getDestinationByMimeTypeConfiguration = ( + allowedMimeTypes?: string | string[] | Record +) => { + const fallback: Record = { + audio: { + directory: 'audio', + rename: false, + }, + image: { + directory: 'images', + rename: true, + }, + video: { + directory: 'videos', + rename: true, + }, + '*': { + directory: 'files', + rename: false, + }, + }; + + const mimeTypeArrayReducer = ( + acc: Record, + curr: string + ) => { + if (fallback[curr]) acc[curr] = fallback[curr]; + else + acc[curr] = { + directory: `${curr}s`, + rename: false, + }; + + return acc; + }; + + if (!allowedMimeTypes) return fallback; + + if (typeof allowedMimeTypes === 'string') + return allowedMimeTypes + .split(',') + .map((val) => val.trim()) + .reduce(mimeTypeArrayReducer, {}); + + if (Array.isArray(allowedMimeTypes)) + return (allowedMimeTypes as string[]).reduce(mimeTypeArrayReducer, {}); + + if (typeof allowedMimeTypes === 'object') return allowedMimeTypes; + + return fallback; +}; + +/** + * + * @param initialConfiguration + * @returns + */ +export default function getConfiguration( + initialConfiguration: TwtKprPluginConfiguration +) { + const { + mainRoute = env.TWTKPR_DEFAULT_ROUTE, + privateDirectory = env.TWTKPR_PRIVATE_DIRECTORY, + publicDirectory = env.TWTKPR_PUBLIC_DIRECTORY, + twtxtFilename = env.TWTKPR_TWTXT_FILENAME, + postLimiterConfiguration, + queryParameters, + uploadConfiguration, + } = initialConfiguration ?? {}; + + const { + active: postLimiterActive = env.TWTKPR_POST_LIMITER_ACTIVE, + ...otherPostLimiterProps + } = postLimiterConfiguration ?? {}; + + const { + app = env.TWTKPR_QUERY_PARAMETER_APP, + css = env.TWTKPR_QUERY_PARAMETER_CSS, + following = env.TWTKPR_QUERY_PARAMETER_FOLLOWING, + js = env.TWTKPR_QUERY_PARAMETER_JS, + logout = env.TWTKPR_QUERY_PARAMETER_LOGOUT, + metadata = env.TWTKPR_QUERY_PARAMETER_METADATA, + twt = env.TWTKPR_QUERY_PARAMETER_TWT, + twts = env.TWTKPR_QUERY_PARAMETER_TWTS, + } = queryParameters ?? {}; + + const { + active: uploadActive = env.TWTKPR_UPLOAD_ACTIVE, + allowEmptyFiles = env.TWTKPR_UPLOAD_ALLOW_EMPTY_FILES, + allowedMimeTypes = env.TWTKPR_UPLOAD_ALLOWED_MIME_TYPES, + createDirsFromUploads = env.TWTKPR_UPLOAD_CREATE_DIRS_FROM_UPLOADS, + directory = env.TWTKPR_UPLOAD_DIRECTORY, + encoding = env.TWTKPR_UPLOAD_ENCODING, + fileWriteStreamHandler, + filter = () => true, + hashAlgorithm = env.TWTKPR_UPLOAD_HASH_ALGORITHM, + keepExtensions = env.TWTKPR_UPLOAD_KEEP_EXTENSIONS, + maxFields = env.TWTKPR_UPLOAD_MAX_FIELDS, + maxFileSize = env.TWTKPR_UPLOAD_MAX_FIELDS_SIZE, + maxFiles = env.TWTKPR_UPLOAD_MAX_FILES, + maxTotalFileSize = env.TWTKPR_UPLOAD_MAX_TOTAL_FILE_SIZE, + minFileSize = env.TWTKPR_UPLOAD_MIN_FILE_SIZE, + route = env.TWTKPR_UPLOAD_ROUTE, + } = uploadConfiguration ?? {}; + + return { + // secrets cannot be provided through configuration file, must use ENV / .env + accessSecret: env.TWTKPR_ACCESS_SECRET, + refreshSecret: env.TWTKPR_REFRESH_SECRET, + mainRoute, + privateDirectory, + publicDirectory, + twtxtFilename, + postLimiterConfiguration: { + active: postLimiterActive, + ...(otherPostLimiterProps ?? {}), + }, + queryParameters: { + ...queryParameters, + app, + css, + following, + js, + logout, + metadata, + twt, + twts, + }, + uploadConfiguration: { + ...uploadConfiguration, + active: uploadActive, + allowEmptyFiles, + allowedMimeTypes: getDestinationByMimeTypeConfiguration(allowedMimeTypes), + createDirsFromUploads, + directory, + encoding, + fileWriteStreamHandler, + filter, + hashAlgorithm: hashAlgorithm as string | false | undefined, + keepExtensions, + maxFields, + maxFileSize, + maxFiles, + maxTotalFileSize, + minFileSize, + route, + }, + } as TwtKprConfiguration; +} diff --git a/src/lib/refreshTokensDB.ts b/src/lib/refreshTokensDB.ts new file mode 100644 index 0000000..3581e77 --- /dev/null +++ b/src/lib/refreshTokensDB.ts @@ -0,0 +1,58 @@ +import arrayDB from './arrayDB.js'; +import Debug from 'debug'; +import jwt from 'jsonwebtoken'; + +export interface RefreshTokensDB { + cleanUp: () => void; + get: (key: string) => string[]; + getObject: () => Record; + remove: (key?: string) => void; + set: (key?: string, value?: string[]) => string[]; +} + +const debug = Debug('twtkpr:simpleDB'); + +/** + * + * @param directory + * @returns + */ +export default async function refreshTokensDB(directory: string) { + const refreshTokensDB = await arrayDB('refreshTokens', directory); + + const get = (key: string) => { + const currentTime = Math.floor(Date.now() / 1000); + debug('get', key, currentTime); + + return (refreshTokensDB.get(key) ?? []).filter((token) => { + const val = jwt.decode(token); + return val && ((val as jwt.JwtPayload).exp ?? 0) >= currentTime; + }); + }; + + const cleanUp = () => { + const currentTime = Math.floor(Date.now() / 1000); + const tokenListByUserId = refreshTokensDB.getObject(); + + debug('cleanup', currentTime); + + Object.keys(tokenListByUserId).forEach((userId) => { + const tokens = refreshTokensDB.get(userId).filter((token) => { + const val = jwt.decode(token); + return val && ((val as jwt.JwtPayload).exp ?? 0) >= currentTime; + }); + + debug(`setting tokens for ${userId}`, tokens); + + refreshTokensDB.set(userId, tokens); + }); + }; + + cleanUp(); + + return { + ...refreshTokensDB, + cleanUp, + get, + } as RefreshTokensDB; +} diff --git a/src/lib/simpleDB.ts b/src/lib/simpleDB.ts new file mode 100644 index 0000000..c36fddc --- /dev/null +++ b/src/lib/simpleDB.ts @@ -0,0 +1,89 @@ +import Debug from 'debug'; +import path from 'node:path'; + +import { loadObjectFromJson, saveToJson } from './utils.js'; + +const debug = Debug('twtkpr:simpleDB'); + +/** + * + * @param name + * @param directory + * @returns + */ +export default async function simpleDB(name: string, directory: string) { + let theName: string; + let dataObject: Record; + + const get = (key = '') => { + debug('get', { key }); + + if (!theName || !dataObject) + throw new Error('DB must be initialized first'); + + key = key?.trim(); + if (!key) throw new Error('a valid key must be provided'); + + return dataObject[key]; + }; + + const getObject = () => dataObject; + + const initialize = async (dbName = '') => { + debug('initialize starting', { dbName }); + + dbName = dbName?.trim(); + if (!dbName) throw new Error('a valid name must be provided'); + + try { + dataObject = await loadObjectFromJson( + path.join(directory, `${dbName}.json`) + ); + } catch (err: unknown) { + debug('initialize read error', { err }); + + if ((err as { code: string }).code === 'ENOENT') dataObject = {}; + else throw err; + } + + // only initialize (and set name) if everything passes + theName = dbName; + debug('initialize complete', { dataObject, name: theName }); + }; + + const remove = (key = '') => { + debug('remove', { key }); + + if (!theName || !dataObject) + throw new Error('DB must be initialized first'); + + key = key?.trim(); + if (!key) throw new Error('a valid key must be provided'); + + delete dataObject[key]; + }; + + const set = (key = '', value = '') => { + debug('set', { key }); + + if (!theName || !dataObject) + throw new Error('DB must be initialized first'); + + key = key?.trim(); + if (!key) throw new Error('a valid key must be provided'); + + dataObject[key] = value; + saveToJson(dataObject, path.join(directory, `${name}.json`)); + + return value; + }; + + await initialize(name); + + return { + get, + getObject, + remove, + set, + }; +} diff --git a/src/lib/twtxtCache.ts b/src/lib/twtxtCache.ts new file mode 100644 index 0000000..ddb9ff9 --- /dev/null +++ b/src/lib/twtxtCache.ts @@ -0,0 +1,48 @@ +import fsp from 'node:fs/promises'; +import path from 'node:path'; + +import { NodeCache } from '@cacheable/node-cache'; +import Debug from 'debug'; + +import { parseTwtxt } from 'twtxt-lib'; +import { TwtKprConfiguration } from '../types.js'; + +/** + * + * @param param0 + * @returns + */ +export default function twtxtCache({ + publicDirectory, + twtxtFilename, +}: Pick) { + let isLoaded = false; + + const debug = Debug('twtkpr:twtxtCache'); + + const cache = new NodeCache(); + + const reloadCache = async () => { + const fileText = await fsp.readFile( + path.join(publicDirectory, twtxtFilename), + 'utf8' + ); + + const parsedFile = parseTwtxt(fileText); + Object.keys(parsedFile).forEach((key) => { + cache.set(key, parsedFile[key as keyof typeof parsedFile]); // 10 seconds + }); + + cache.set('source', fileText); + debug(`cache ${isLoaded ? 're' : ''}loaded`); + + isLoaded = true; + }; + + reloadCache(); + + return { + cache, + reloadCache, + }; +} diff --git a/src/lib/userDB.ts b/src/lib/userDB.ts new file mode 100644 index 0000000..1242bec --- /dev/null +++ b/src/lib/userDB.ts @@ -0,0 +1,17 @@ +import simpleDB from './simpleDB.js'; + +export interface UserDB { + get: (key?: string) => string; + getObject: () => Record; + remove: (key?: string) => void; + set: (key?: string, value?: string) => string; +} + +/** + * + * @param directory + * @returns + */ +export default function userDB(directory: string) { + return simpleDB('user', directory) as Promise; +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..a8eca94 --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,88 @@ +import crypto from 'node:crypto'; +import { readFile, writeFile } from 'node:fs/promises'; +import jwt from 'jsonwebtoken'; +import { v4 as uuidv4 } from 'uuid'; + +/** + * + * @param userId + * @param secret + * @returns + */ +export const generateAccessToken = (userId: string, secret = '') => + jwt.sign({ id: userId }, secret, { expiresIn: '10m' }); + +/** + * + * @param val + * @returns + */ +export const generateEtag = (val: string) => + crypto.createHash('sha256').update(val).digest('hex'); + +/** + * + * @param userId + * @param secret + * @param extendRefresh + * @returns + */ +export const generateRefreshToken = ( + userId: string, + secret = '', + extendRefresh = false +) => { + const tokenId = uuidv4(); // unique ID for the refresh token + + const token = jwt.sign({ id: userId, tokenId }, secret, { + expiresIn: extendRefresh ? '7d' : '1h', + }); + + return token; +}; + +/** + * + * @param value + * @returns + */ +export const getQueryParameterArray = (value: unknown | unknown[] = []) => + Array.isArray(value) + ? value.map((val) => `${val}`.trim()) + : [`${value}`.trim()]; + +/** + * + * @param value + * @returns + */ +export const getValueOrFirstEntry = (value: string | string[]) => + Array.isArray(value) && value.length ? value[0] : value; + +/** + * + * @param filePath + * @returns + */ +export const loadObjectFromJson = async (filePath: string) => { + const contents = await readFile(filePath, { encoding: 'utf8' }); + return JSON.parse(contents); +}; + +/** + * + * @param contents + * @param filePath + */ +export const saveToJson = async ( + contents: object | string, + filePath: string +) => { + const stringContents = + typeof contents === 'string' ? contents : JSON.stringify(contents, null, 2); + + await writeFile(filePath, stringContents, { + encoding: 'utf8', + flag: 'w', + }); +}; diff --git a/src/middlewares/authCheckJWT.ts b/src/middlewares/authCheckJWT.ts new file mode 100644 index 0000000..4b3d325 --- /dev/null +++ b/src/middlewares/authCheckJWT.ts @@ -0,0 +1,43 @@ +import type { Request } from 'express'; + +import Debug from 'debug'; +import jwt from 'jsonwebtoken'; + +import { TwtKprConfiguration } from '../types.js'; + +const debug = Debug('twtkpr:authCheckJWT'); + +/** + * Checks for a valid JWT, and returns a boolean indicating the result + * + * @param req + * @returns + */ +export default async function authCheckJWT( + req: Request, + config: TwtKprConfiguration +) { + debug('beginning'); + + const token = req.header('Authorization')?.split(' ')[1]; + if (!token) { + debug('no token'); + return false; + } + + debug('token present'); + + try { + const decoded = jwt.verify(token, config.accessSecret) as { id: string }; + debug({ decoded }); + + if (!decoded.id) return false; + req.username = decoded.id; + } catch { + debug('invalid token'); + return false; + } + + debug('token good'); + return true; +} diff --git a/src/middlewares/csrfProtection.ts b/src/middlewares/csrfProtection.ts new file mode 100644 index 0000000..db31b42 --- /dev/null +++ b/src/middlewares/csrfProtection.ts @@ -0,0 +1,25 @@ +/* +import { doubleCsrf } from "csrf-csrf"; + +const { + invalidCsrfTokenError, // This is just for convenience if you plan on making your own middleware. + generateCsrfToken, // Use this in your routes to provide a CSRF token. + validateRequest, // Also a convenience if you plan on making your own middleware. + doubleCsrfProtection, // This is the default CSRF protection middleware. +} = doubleCsrf({ + getSecret: (req) => 'return some cryptographically pseudorandom secret here', + getSessionIdentifier: (req) => req.session.id // return the requests unique identifier +}); + + +const csrfTokenRoute = (req, res) => { + const csrfToken = generateCsrfToken(req, res); + // You could also pass the token into the context of a HTML response. + res.json({ csrfToken }); +}; + +export { + csrfTokenRoute, + doubleCsrfProtection, +} +*/ diff --git a/src/middlewares/index.ts b/src/middlewares/index.ts new file mode 100644 index 0000000..0d9fe81 --- /dev/null +++ b/src/middlewares/index.ts @@ -0,0 +1,4 @@ +export { default as authCheck } from './authCheckJWT.js'; +export { default as memoryCache } from './postHandler/memoryCache.js'; +export { default as postHandler } from './postHandler/index.js'; +export { default as queryHandler } from './queryHandler/index.js'; diff --git a/src/middlewares/postHandler/index.ts b/src/middlewares/postHandler/index.ts new file mode 100644 index 0000000..401cc49 --- /dev/null +++ b/src/middlewares/postHandler/index.ts @@ -0,0 +1 @@ +export { default } from "./postHandler.js"; diff --git a/src/middlewares/postHandler/login.ts b/src/middlewares/postHandler/login.ts new file mode 100644 index 0000000..9828aba --- /dev/null +++ b/src/middlewares/postHandler/login.ts @@ -0,0 +1,86 @@ +import type { Request, Response } from 'express'; + +import bcrypt from 'bcryptjs'; +import Debug from 'debug'; + +import { env } from '../../lib/env.js'; +import refreshTokensDB, { RefreshTokensDB } from '../../lib/refreshTokensDB.js'; +import userDB, { UserDB } from '../../lib/userDB.js'; +import { + generateAccessToken, + generateEtag, + generateRefreshToken, +} from '../../lib/utils.js'; +import { TwtKprConfiguration } from '../../types.js'; + +const debug = Debug('twtkpr:login'); + +/** + * Handles login request and (if successful) returns the JWT access token wile setting the refresh n the + * + * @param req + * @param res + * @returns + */ +export default async function loginHandler( + req: Request, + res: Response, + config: TwtKprConfiguration +) { + const { accessSecret, privateDirectory, refreshSecret } = config; + debug('starting'); + + try { + const tokens = await refreshTokensDB(privateDirectory); + const users = await userDB(privateDirectory); + + const { username, password, rememberToggle } = req.body; + + if (!username || !password || !users.get(username)) { + debug('no values found', username); + + res.status(401).end(); + return; + } + + const isMatch = await bcrypt.compare(password, users.get(username)); + + if (!isMatch) { + privateDirectory; + debug('no match'); + + res.status(401).end(); + return; + } + + debug('generating tokens'); + + const accessToken = generateAccessToken(username, accessSecret); + debug(`access token: ${accessToken}`); + + const refreshToken = generateRefreshToken( + username, + refreshSecret, + !!rememberToggle + ); + debug(`refresh token: ${refreshToken}`); + + debug('setting tokens'); + tokens.set(username, (tokens.get(username) || []).concat([refreshToken])); + + debug('setting refreshToken cookie'); + res.cookie('refreshToken', refreshToken, { + httpOnly: true, + secure: env.NODE_ENV === 'production', + sameSite: 'strict', + // 1 hour or 7 days + maxAge: (rememberToggle ? 1 : 7 * 24) * 60 * 60 * 1000, + }); + + debug('setting response'); + res.set('etag', generateEtag(accessToken)).status(200).send(accessToken); + } catch (err) { + console.error(err); + res.status(500).end(); + } +} diff --git a/src/middlewares/postHandler/logout.ts b/src/middlewares/postHandler/logout.ts new file mode 100644 index 0000000..ac3842d --- /dev/null +++ b/src/middlewares/postHandler/logout.ts @@ -0,0 +1,31 @@ +import type { Request, Response } from 'express'; + +import Debug from 'debug'; + +import { TwtKprConfiguration } from '../../types.js'; + +const debug = Debug('twtkpr:logout'); + +/** + * Handles logout request and clears the token cookies + * + * @param req + * @param res + * @returns + */ +export default async function logoutHandler( + req: Request, + res: Response, + config: TwtKprConfiguration +) { + const { mainRoute } = config; + debug('logging out'); + + res + .status(200) + .clearCookie('refreshToken') + .clearCookie('accessToken') + .redirect(mainRoute); + + return; +} diff --git a/src/middlewares/postHandler/memoryCache.ts b/src/middlewares/postHandler/memoryCache.ts new file mode 100644 index 0000000..e28ae54 --- /dev/null +++ b/src/middlewares/postHandler/memoryCache.ts @@ -0,0 +1,36 @@ +import type { NextFunction, Request, Response } from 'express'; +import NodeCache from '@cacheable/node-cache'; + +import Debug from 'debug'; + +const debug = Debug('twtkpr:memoryCache'); + +/** + * + * @param req + * @param res + * @param next + * @param cache + * @param reloadCache + * @returns + */ +export default async function memoryCache( + req: Request, + res: Response, + next: NextFunction, + cache: NodeCache, + reloadCache: () => Promise +) { + if (cache.keys().length && !['DELETE', 'POST', 'PUT'].includes(req.method)) { + next(); + return; + } + + reloadCache() + .then(() => { + next(); + }) + .catch((err) => { + console.error(err); + }); +} diff --git a/src/middlewares/postHandler/postHandler.ts b/src/middlewares/postHandler/postHandler.ts new file mode 100644 index 0000000..3d37639 --- /dev/null +++ b/src/middlewares/postHandler/postHandler.ts @@ -0,0 +1,69 @@ +import Debug from 'debug'; +import express, { NextFunction, Request, Response } from 'express'; +import rateLimit from 'express-rate-limit'; + +import authCheck from '../../middlewares/authCheckJWT.js'; +import { TwtKprConfiguration } from '../../types.js'; +import login from './login.js'; +import logout from './logout.js'; +import refresh from './refresh.js'; +import twt from './twt.js'; +import editFile from '../putHandler/editFile.js'; + +const debug = Debug('twtkpr:postHandler'); + +/** + * + * @param config + * @returns + */ +export default function postHandler(config: TwtKprConfiguration) { + const { postLimiterConfiguration } = config; + const { active: isLimiterActive, ...otherLimiterProps } = + postLimiterConfiguration ?? {}; + + const postLimiter = isLimiterActive + ? rateLimit({ + ...otherLimiterProps, + }) + : (req: Request, res: Response, next: NextFunction) => { + next(); + }; + + const { mainRoute } = config; + + const router = express.Router(); + + router.post('/', postLimiter, async (req, res, next) => { + const { content, type } = req.body ?? {}; + debug('post', { type, path: req.path }); + + if (type === 'logout') { + debug('logging out'); + res.clearCookie('refreshToken'); + res.clearCookie('accessToken'); + res.redirect(mainRoute); + return; + } + + if (type === 'login') return login(req, res, config); + if (type === 'logout') return logout(req, res, config); + if (type === 'refresh') return refresh(req, res, config); + + debug('checking auth'); + const isLoggedIn = await authCheck(req, config); + if (!isLoggedIn) { + debug('auth check failed'); + next(); + return; + } + debug('auth check succeeded'); + + if (type === 'twt' || content) return twt(req, res, config); + if (type === 'editFile') return editFile(req, res, config); + + next(); + }); + + return router; +} diff --git a/src/middlewares/postHandler/refresh.ts b/src/middlewares/postHandler/refresh.ts new file mode 100644 index 0000000..c3149e0 --- /dev/null +++ b/src/middlewares/postHandler/refresh.ts @@ -0,0 +1,117 @@ +import type { Request, Response } from 'express'; + +import Debug from 'debug'; +import jwt from 'jsonwebtoken'; + +import { env } from '../../lib/env.js'; +import refreshTokensDB from '../../lib/refreshTokensDB.js'; +import { + generateAccessToken, + generateEtag, + generateRefreshToken, +} from '../../lib/utils.js'; +import { TwtKprConfiguration } from '../../types.js'; + +const debug = Debug('twtkpr:refresh'); + +/** + * Issues a new JWT and updates the refresh token in the cookie + * + * @param req + * @param res + */ +export default async function refresh( + req: Request, + res: Response, + config: TwtKprConfiguration +) { + const send401 = (message: string) => { + debug(message); + + res + .clearCookie('accessToken') + .clearCookie('refreshToken') + .status(401) + .send(message ?? 'Unauthorized'); + + return; + }; + + try { + const tokens = await refreshTokensDB(config.privateDirectory); + const oldToken = req.cookies.refreshToken; + + debug(oldToken); + + if (!oldToken) return send401('Unauthorized'); + + let decoded = { id: '' }; + + try { + decoded = jwt.verify(oldToken, config.refreshSecret) as { + id: string; + }; + + debug({ decoded }); + } catch (err) { + return send401('Refresh token invalid'); + } + + const username = req.username ?? decoded.id; + + if (!username) return send401('Missing username'); + + const currentTime = Math.floor(Date.now() / 1000); + + // cleanup tokens on load + const validTokens = (tokens.get(decoded.id) ?? []).filter((token) => { + const val = jwt.decode(token); + return val && ((val as jwt.JwtPayload).exp ?? 0) >= currentTime; + }); + + // If token is invalid or not the latest one + if (!validTokens.includes(oldToken)) { + debug('token missing from list'); + return send401('Invalid refresh token'); + } + + debug('generating new tokens'); + + const newAccessToken = generateAccessToken( + req.username || decoded.id, + config.accessSecret + ); + + const newRefreshToken = generateRefreshToken( + req.username || decoded.id, + config.refreshSecret + ); + + debug('updating token list'); + tokens.set( + req.username || decoded.id, + validTokens + .filter((token) => token !== oldToken) + .concat([newRefreshToken]) + ); + + debug('setting httpOnly cookie with new refresh token'); + res.cookie('refreshToken', newRefreshToken, { + httpOnly: true, + secure: env.NODE_ENV === 'production', + sameSite: 'strict', + // 1 hour or 7 days + maxAge: (!!req.query.rememberToggle ? 1 : 7 * 24) * 60 * 60 * 1000, + }); + + // Return the new access token in body + debug('generating response'); + res + .set('etag', generateEtag(newAccessToken)) + .status(200) + .send(newAccessToken); + } catch (err) { + console.error(err); + res.status(500).end(); + } +} diff --git a/src/middlewares/postHandler/twt.ts b/src/middlewares/postHandler/twt.ts new file mode 100644 index 0000000..542adc9 --- /dev/null +++ b/src/middlewares/postHandler/twt.ts @@ -0,0 +1,36 @@ +import type { Request, Response } from 'express'; + +import dayjs from 'dayjs'; +import fs from 'node:fs'; +import { join } from 'node:path'; + +import { TwtKprConfiguration } from '../../types.js'; + +/** + * Creates a new twt, appending it to the bottom of the TWTXT file + * + * @param req + * @param res + */ +export default function twt( + req: Request, + res: Response, + config: TwtKprConfiguration +) { + const { content } = req.body ?? {}; + + const date = dayjs().format(); + const twt = `${date}\t${content.trim()}\n`; + + const stream = fs.createWriteStream( + join(config.publicDirectory, config.twtxtFilename), + { + flags: 'a', + } + ); + + stream.write(twt); + stream.end(); + + res.status(200).send(twt); +} diff --git a/src/middlewares/putHandler/editFile.ts b/src/middlewares/putHandler/editFile.ts new file mode 100644 index 0000000..f3aa655 --- /dev/null +++ b/src/middlewares/putHandler/editFile.ts @@ -0,0 +1,38 @@ +import type { Request, Response } from "express"; + +import fs from "node:fs"; +import path from "node:path"; + +import { TwtKprConfiguration } from "../../types.js"; + +/** + * Creates a new twt, appending it to the bottom of the TWTXT file + * + * @param req + * @param res + */ +export default function editFile( + req: Request, + res: Response, + config: TwtKprConfiguration, +) { + const { fileContents } = req.body ?? {}; + + if (!fileContents) { + res.status(400).send("Missing fileContents"); + return; + } + + const stream = fs.createWriteStream( + path.join(config.publicDirectory, config.twtxtFilename), + { + flags: "w", + start: 0, + }, + ); + + stream.write(fileContents); + stream.end(); + + res.type("text").status(200).send(fileContents); +} diff --git a/src/middlewares/putHandler/index.ts b/src/middlewares/putHandler/index.ts new file mode 100644 index 0000000..30a6615 --- /dev/null +++ b/src/middlewares/putHandler/index.ts @@ -0,0 +1 @@ +export { default } from "./putHandler.js"; diff --git a/src/middlewares/putHandler/putHandler.ts b/src/middlewares/putHandler/putHandler.ts new file mode 100644 index 0000000..7a56e1e --- /dev/null +++ b/src/middlewares/putHandler/putHandler.ts @@ -0,0 +1,34 @@ +import Debug from 'debug'; +import express from 'express'; + +import authCheck from '../../middlewares/authCheckJWT.js'; +import { TwtKprConfiguration } from '../../types.js'; +import editFile from './editFile.js'; + +const debug = Debug('twtkpr:putHandler'); + +/** + * + * @param config + * @returns + */ +export default function putHandler(config: TwtKprConfiguration) { + const router = express.Router(); + + router.put('/', (req, res, next) => { + debug('put', { path: req.path }); + + debug('checking auth'); + + if (!authCheck(req, config)) { + debug('auth check failed'); + next(); + return; + } + + debug('auth check succeeded'); + + return editFile(req, res, config); + }); + return router; +} diff --git a/src/middlewares/queryHandler/followingHandler.ts b/src/middlewares/queryHandler/followingHandler.ts new file mode 100644 index 0000000..b81a780 --- /dev/null +++ b/src/middlewares/queryHandler/followingHandler.ts @@ -0,0 +1,65 @@ +import type { Request, Response } from 'express'; +import type { Twttr } from 'twtxt-lib'; + +import { + generateEtag, + getQueryParameterArray, + getValueOrFirstEntry, +} from '../../lib/utils.js'; +import { QueryParameters } from '../../types.js'; +import NodeCache from '@cacheable/node-cache'; + +/** + * + * @param req + * @param res + * @param cache + * @param followingParameter + */ +export default function followingHandler( + req: Request, + res: Response, + cache: NodeCache, + followingParameter: QueryParameters['following'] +) { + const followingsToMatch = getQueryParameterArray( + req.query[followingParameter] + ); + + const nicksToMatch = getQueryParameterArray(req.query.nick); + const urlsToMatch = getQueryParameterArray(req.query.url); + + const searchTermsToMatch = [ + ...getQueryParameterArray(req.query.search), + ...getQueryParameterArray(req.query.s), + ]; + + const wantsJson = + req.is('json') || + getValueOrFirstEntry(getQueryParameterArray(req.query.format)) === 'json'; + if (wantsJson) res.set('content-type', 'application/json'); + else res.set('content-type', 'text/plain'); + + const matchedFollowing = (cache.get('following') as Twttr[]).filter( + ({ nick, url }) => + (!followingsToMatch.length || + (followingsToMatch.length === 1 && followingsToMatch[0] === '') || + followingsToMatch.includes(nick) || + followingsToMatch.includes(`@${nick}`) || + followingsToMatch.includes(url)) && + (!nicksToMatch.length || + nicksToMatch.includes(nick) || + nicksToMatch.includes(`@${nick}`)) && + (!urlsToMatch.length || urlsToMatch.includes(url)) && + (!searchTermsToMatch.length || + searchTermsToMatch.some( + (term) => nick.includes(term) || url.includes(term) + )) + ); + + const result = wantsJson + ? JSON.stringify(matchedFollowing) + : matchedFollowing.map(({ nick, url }) => `@${nick} ${url}`).join('\n'); + + res.set('etag', generateEtag(result)).send(result); +} diff --git a/src/middlewares/queryHandler/index.ts b/src/middlewares/queryHandler/index.ts new file mode 100644 index 0000000..9f0a132 --- /dev/null +++ b/src/middlewares/queryHandler/index.ts @@ -0,0 +1 @@ +export { default } from "./queryHandler.js"; diff --git a/src/middlewares/queryHandler/metadataHandler.ts b/src/middlewares/queryHandler/metadataHandler.ts new file mode 100644 index 0000000..f24e156 --- /dev/null +++ b/src/middlewares/queryHandler/metadataHandler.ts @@ -0,0 +1,92 @@ +import type { Request, Response } from 'express'; +import type { Metadata } from 'twtxt-lib'; + +import { env } from '../../lib/env.js'; +import twtxtCache from '../../lib/twtxtCache.js'; +import { + generateEtag, + getQueryParameterArray, + getValueOrFirstEntry, +} from '../../lib/utils.js'; +import NodeCache from '@cacheable/node-cache'; +import { QueryParameters } from '../../types.js'; + +export interface MetadataHandler { + cache: NodeCache; + metadataParameter: QueryParameters['metadata']; + req: Request; + res: Response; +} + +/** + * + * @param req + * @param res + * @param cache + * @param metadataParameter + */ +export default function metadataHandler( + req: Request, + res: Response, + cache: NodeCache, + metadataParameter: QueryParameters['metadata'] +) { + const metadataToMatch = getQueryParameterArray(req.query[metadataParameter]); + + const searchTermsToMatch = [ + ...getQueryParameterArray(req.query.search), + ...getQueryParameterArray(req.query.s), + ]; + + const metadata = (cache.get('metadata') as Metadata) ?? {}; + + const wantsJson = + req.is('json') || + getValueOrFirstEntry(getQueryParameterArray(req.query.format)) === 'json'; + if (wantsJson) res.set('content-type', 'application/json'); + else res.set('content-type', 'text/plain'); + + const matchedMetadata = Object.keys(metadata) + .filter( + (key) => + (!metadataToMatch.length || + (metadataToMatch.length === 1 && metadataToMatch[0] === '') || + metadataToMatch.includes(key)) && + (!searchTermsToMatch.length || + searchTermsToMatch.some((term) => + key.includes(term) || Array.isArray(metadata[key]) + ? (metadata[key] as string[]).some((val) => val.includes(term)) + : metadata[key].includes(term) + )) + ) + .reduce( + (acc, key) => { + const value = metadata[key as keyof typeof metadata]; + acc[key] = Array.isArray(value) + ? value.filter( + (value) => + !searchTermsToMatch.length || + searchTermsToMatch.some( + (term) => key.includes(term) || value.includes(term) + ) + ) + : value; + return acc; + }, + {} as Record + ); + + const result = wantsJson + ? JSON.stringify(matchedMetadata) + : Object.keys(matchedMetadata) + .map((key) => { + const value = matchedMetadata[key as keyof typeof matchedMetadata]; + + return Array.isArray(value) + ? value.map((rowVal) => `${key}: ${rowVal}`).join('\n') + : `${key}: ${value}`; + }) + .join('\n'); + + res.set('etag', generateEtag(result)).send(result); +} diff --git a/src/middlewares/queryHandler/queryHandler.ts b/src/middlewares/queryHandler/queryHandler.ts new file mode 100644 index 0000000..4777290 --- /dev/null +++ b/src/middlewares/queryHandler/queryHandler.ts @@ -0,0 +1,88 @@ +import path from 'node:path'; +import NodeCache from '@cacheable/node-cache'; +import Debug from 'debug'; +import type { NextFunction, Request, Response } from 'express'; +import { __dirname } from '../../lib/constants.js'; +import { generateEtag } from '../../lib/utils.js'; +import { TwtKprConfiguration } from '../../types.js'; +import renderApp from '../renderApp/index.js'; +import followingHandler from './followingHandler.js'; +import metadataHandler from './metadataHandler.js'; +import twtHandler from './twtHandler.js'; +import { Twt } from 'twtxt-lib'; + +const debug = Debug('twtkpr:queryHandler'); + +/** + * + * @param config + * @param cache + * @param verifyAuthRequest + * @returns + */ +export default function queryHandler( + config: TwtKprConfiguration, + cache: NodeCache, + verifyAuthRequest: (r: Request) => Promise +) { + const { mainRoute, queryParameters, uploadConfiguration } = config; + + return async (req: Request, res: Response, next: NextFunction) => { + debug({ query: JSON.stringify(req.query) }); + + if (!Object.keys(req.query).length) { + next(); + return; + } + + if (req.query[queryParameters.app] !== undefined) { + const appContent = renderApp({ mainRoute, uploadConfiguration }); + res.set('etag', generateEtag(appContent)).send(appContent); + return; + } + + if (req.query[queryParameters.css] !== undefined) { + res.sendFile('styles.css', { + root: path.resolve(__dirname, 'client'), + }); + return; + } + + if (req.query[queryParameters.js] !== undefined) { + res.sendFile('script.js', { + root: path.resolve(__dirname, 'client'), + }); + return; + } + + if ( + req.query[queryParameters.following] !== undefined && + cache.get('following') + ) { + return followingHandler(req, res, cache, queryParameters.following); + } + + if ( + req.query[queryParameters.metadata] !== undefined && + cache.get('metadata') + ) { + return metadataHandler(req, res, cache, queryParameters.metadata); + } + + if ( + (req.query[queryParameters.twt] !== undefined || + req.query[queryParameters.twts] !== undefined) && + cache.get('twts') + ) { + return twtHandler( + req, + res, + cache.get('twts') as Twt[], + queryParameters.twt, + queryParameters.twts + ); + } + + next(); + }; +} diff --git a/src/middlewares/queryHandler/twtHandler.ts b/src/middlewares/queryHandler/twtHandler.ts new file mode 100644 index 0000000..b1de97e --- /dev/null +++ b/src/middlewares/queryHandler/twtHandler.ts @@ -0,0 +1,101 @@ +import type { Request, Response } from 'express'; +import type { Metadata, Twt } from 'twtxt-lib'; + +import { + generateEtag, + getQueryParameterArray, + getValueOrFirstEntry, +} from '../../lib/utils.js'; +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc.js'; +import NodeCache from '@cacheable/node-cache'; +import { QueryParameters } from '../../types.js'; + +dayjs.extend(utc); + +/** + * + * @param req + * @param res + * @param twts + * @param twtParameter + * @param twtsParameter + * @returns + */ +export default function twtHandler( + req: Request, + res: Response, + twts: Twt[] = [], + twtParameter: QueryParameters['twt'], + twtsParameter: QueryParameters['twts'] +) { + const twtsToMatch = getQueryParameterArray(req.query[twtsParameter]); + const showLastTwt = getQueryParameterArray(req.query[twtParameter]); + const hashesToMatch = getQueryParameterArray(req.query.hash); + const searchTermsToMatch = [ + ...getQueryParameterArray(req.query.search), + ...getQueryParameterArray(req.query.s), + ]; + + const createdDatesToMatch = getQueryParameterArray(req.query.created_date); + const createdUTCStartDatesToMatch = getQueryParameterArray( + req.query.created_date_start + ).map((val) => dayjs.utc(val)); + const createdUTCEndDatesToMatch = getQueryParameterArray( + req.query.created_date_end + ).map((val) => dayjs.utc(val)); + + const wantsJson = + req.is('json') || + getValueOrFirstEntry(getQueryParameterArray(req.query.format)) === 'json'; + if (wantsJson) res.set('content-type', 'application/json'); + else res.set('content-type', 'text/plain'); + + if (showLastTwt.length === 1 && showLastTwt[0] === '') { + const lastTwt = twts.reduce((matched, curr) => + matched?.createdUTC > curr.createdUTC ? matched : curr + ); + let result = 'No results'; + + if (lastTwt) { + result = wantsJson + ? JSON.stringify(lastTwt) + : `${lastTwt?.created || ''}\t${lastTwt?.content || ''}\n`; + } + + res.set('etag', generateEtag(result)).send(result); + return; + } + + const matchedTwts = twts.filter(({ content, created, createdUTC, hash }) => { + return ( + (!twtsToMatch.length || + (twtsToMatch.length === 1 && twtsToMatch[0] === '') || + twtsToMatch.includes(created) || + (hash && + (twtsToMatch.includes(hash) || twtsToMatch.includes(`#${hash}`)))) && + (!hashesToMatch.length || + (hash && + (hashesToMatch.includes(hash) || + hashesToMatch.includes(`#${hash}`)))) && + (!createdDatesToMatch.length || + createdDatesToMatch.some((date) => created.includes(date))) && + (!createdUTCStartDatesToMatch.length || + createdUTCStartDatesToMatch.some( + (date) => date.diff(createdUTC) < 0 + )) && + (!createdUTCEndDatesToMatch.length || + createdUTCEndDatesToMatch.some((date) => date.diff(createdUTC) > 0)) && + (!searchTermsToMatch.length || + searchTermsToMatch.some((term) => content.includes(term))) + ); + }); + + const result = wantsJson + ? JSON.stringify(matchedTwts) + : matchedTwts + .map(({ content, created }) => `${created}\t${content}`) + .join('\n'); + + res.set('etag', generateEtag(result)).send(result); +} diff --git a/src/middlewares/renderApp/index.ts b/src/middlewares/renderApp/index.ts new file mode 100644 index 0000000..4be966c --- /dev/null +++ b/src/middlewares/renderApp/index.ts @@ -0,0 +1 @@ +export { default } from "./renderApp.js"; diff --git a/src/middlewares/renderApp/renderApp.ts b/src/middlewares/renderApp/renderApp.ts new file mode 100644 index 0000000..083f152 --- /dev/null +++ b/src/middlewares/renderApp/renderApp.ts @@ -0,0 +1,148 @@ +import { version } from '../../packageInfo.js'; +import { TwtKprConfiguration } from '../../types.js'; + +import renderUploadButton from './renderUploadButton.js'; + +/** + * + * @param param0 + * @returns + */ +export default function renderApp({ + mainRoute, + uploadConfiguration, +}: Pick) { + return ` + + + + + + + + + + + + TwtKpr + + + + + +
+
+ + +
+

+            
+ +
+ + +
+ + +
+
+ + + + +`; +} diff --git a/src/middlewares/renderApp/renderUploadButton.ts b/src/middlewares/renderApp/renderUploadButton.ts new file mode 100644 index 0000000..c51f210 --- /dev/null +++ b/src/middlewares/renderApp/renderUploadButton.ts @@ -0,0 +1,29 @@ +import { TwtKprConfiguration } from '../../types.js'; + +/** + * + * @param uploadConfiguration + * @param variant + * @returns + */ +export default function renderUploadButton( + uploadConfiguration: TwtKprConfiguration['uploadConfiguration'], + variant: 'normal' | 'small' = 'normal' +) { + const { active, allowedMimeTypes, route } = uploadConfiguration ?? {}; + + if (!active) return ''; + + // determine accept from allowed mime types - may need to rebuild value based on fallback n getConfiguration, rather than at the end. + + return ` + +`; +} diff --git a/src/middlewares/uploadHandler.ts b/src/middlewares/uploadHandler.ts new file mode 100644 index 0000000..678bcc6 --- /dev/null +++ b/src/middlewares/uploadHandler.ts @@ -0,0 +1,174 @@ +import fsp from 'node:fs/promises'; +import path from 'node:path'; +import formidable from 'formidable'; +import type { NextFunction, Request, Response } from 'express'; +import Debug from 'debug'; + +import { __dirname } from '../lib/env.js'; +import { MimeOptions, TwtKprConfiguration } from '../types.js'; + +const debug = Debug('twtkpr:uploadHandler'); + +/** + * + * @param allowedMimeTypes + * @returns + */ +const getDestinationByMimeTypeConfiguration = ( + allowedMimeTypes?: string | string[] | Record +) => { + const fallback: Record = { + audio: 'audio', + image: 'images', + text: 'texts', + video: 'videos', + '*': 'files', + }; + + const mimeTypeArrayReducer = (acc: Record, curr: string) => { + if (fallback[curr]) acc[curr] = fallback[curr]; + else acc[curr] = `${curr}s`; + return acc; + }; + + if (!allowedMimeTypes) return fallback; + + if (typeof allowedMimeTypes === 'string') + return allowedMimeTypes + .split(',') + .map((val) => val.trim()) + .reduce(mimeTypeArrayReducer, {}); + + if (Array.isArray(allowedMimeTypes)) + return (allowedMimeTypes as string[]).reduce(mimeTypeArrayReducer, {}); + + if (typeof allowedMimeTypes === 'object') return allowedMimeTypes; + + return fallback; +}; + +/** + * + * @param config + * @param verifyAuthRequest + * @returns + */ +export default function uploadHandler( + config: TwtKprConfiguration, + verifyAuthRequest: (r: Request) => Promise +) { + return async (req: Request, res: Response, next: NextFunction) => { + debug('checking auth'); + if (!(await verifyAuthRequest(req))) { + debug('auth check failed'); + res.status(401).send('Unauthorized'); + return; + } + debug('auth check succeeded'); + + const { active, allowedMimeTypes, directory, route, ...otherProps } = + config.uploadConfiguration; + + if ( + !active || + (Array.isArray(allowedMimeTypes) && !allowedMimeTypes.length) + ) { + next(); + return; + } + + debug('using configuration: ', { + uploadConfiguration: config.uploadConfiguration, + }); + + const form = formidable({ + uploadDir: directory, + ...otherProps, + }); + + form.parse(req, async (err, fields, files) => { + if (err) { + next(err); + return; + } + const uploadsDir = (route ?? '').replaceAll('/', ''); + + let hadFileError = false; + const processedFiles: string[] = []; + const destinationByMimeType = allowedMimeTypes; + + debug(`processing ${(files?.files ?? []).length} files`); + + for (const file of files?.files ?? []) { + const { filepath, hash, mimetype, newFilename, originalFilename } = + file ?? {}; + if (!(filepath && newFilename && originalFilename)) return; + + console.log({ file }); + + let ext = path.extname(originalFilename).toLocaleLowerCase(); + if (ext === '.jpeg') ext = '.jpg'; + + const finalFilename = ( + hash && (mimetype?.includes('image') || mimetype?.includes('video')) + ? `${hash}${ext}` + : originalFilename + ) + .replace(/\s+/g, '-') + .toLocaleLowerCase(); + + let destinationDir = ''; + Object.keys(destinationByMimeType).forEach((mimeType) => { + if (file.mimetype?.split('/')?.[0] === mimeType.toLocaleLowerCase()) + destinationDir = + ( + destinationByMimeType[ + mimeType as keyof typeof destinationByMimeType + ] as MimeOptions + ).directory ?? ''; + }); + if (destinationDir === '') + destinationDir = + ( + destinationByMimeType[ + '*' as keyof typeof destinationByMimeType + ] as MimeOptions + ).directory ?? uploadsDir; + + const finalPath = path.join(process.cwd(), 'public', destinationDir); + + debug(`creating '${finalPath}'`); + fsp.mkdir(finalPath, { recursive: true }); + + debug(`copying '${filepath}' to '/${destinationDir}/${finalFilename}'`); + + try { + await fsp.copyFile(filepath, path.join(finalPath, finalFilename)); + + debug(`cleaning up '${filepath}'`); + await fsp.rm(filepath); + + debug(`processed successfully`); + processedFiles.push(`/${destinationDir}/${finalFilename}`); + } catch (err) { + debug(`error!`); + hadFileError = true; + console.error(err); + } + } + + debug('generating reply...'); + if (hadFileError && processedFiles.length) { + res.type('text/plain').status(206).send(processedFiles.join('\n')); + return; + } + + if (!processedFiles.length) { + res.type('text/plain').status(500).send('No files processed'); + return; + } + + res.type('text/plain').status(201).send(processedFiles.join('\n')); + }); + }; +} diff --git a/src/packageInfo.ts b/src/packageInfo.ts new file mode 100644 index 0000000..1e9b47b --- /dev/null +++ b/src/packageInfo.ts @@ -0,0 +1,5 @@ +import pkgInfo from "../package.json" with { type: "json" }; + +const { author, name, version } = pkgInfo; + +export { author, name, version }; diff --git a/src/plugin.ts b/src/plugin.ts new file mode 100644 index 0000000..9464fc0 --- /dev/null +++ b/src/plugin.ts @@ -0,0 +1,60 @@ +import cookieParser from 'cookie-parser'; +import Debug from 'debug'; +import express, { Request, Router } from 'express'; +import { TwtKprPluginConfiguration } from './types.js'; + +import authCheck from './middlewares/authCheckJWT.js'; +import twtxtCache from './lib/twtxtCache.js'; +import getConfiguration from './lib/getConfiguration.js'; +import queryHandler from './middlewares/queryHandler/index.js'; +import uploadHandler from './middlewares/uploadHandler.js'; +import postHandler from './middlewares/postHandler/index.js'; +import memoryCache from './middlewares/postHandler/memoryCache.js'; +import putHandler from './middlewares/putHandler/index.js'; + +export default function plugin(initialConfig?: TwtKprPluginConfiguration) { + const debug = Debug('twtkpr:plugin'); + + const router = express.Router(); + + const config = getConfiguration(initialConfig ?? {}); + const { publicDirectory, twtxtFilename } = config; + + const verifyAuthRequest = (req: Request) => authCheck(req, config); + + debug('initializing cache'); + const { cache, reloadCache } = twtxtCache({ publicDirectory, twtxtFilename }); + + debug('adding URL encoder'); + router.use(express.urlencoded({ extended: true })); + + debug('adding cookieParser'); + router.use(cookieParser()); + + debug('adding queryRouter'); + router.use(config.mainRoute, queryHandler(config, cache, verifyAuthRequest)); + + debug(`adding uploadHandler at /${config.uploadConfiguration.route}`); + router.post( + `/${config.uploadConfiguration.route}`, + uploadHandler(config, verifyAuthRequest) + ); + + debug('adding postHandler and putHandler'); + router.use(config.mainRoute, postHandler(config), putHandler(config)); + + debug('adding static'); + router.use(express.static(config.publicDirectory)); + + debug('adding default redirect'); + router.get('/', (_, res) => { + res.redirect(config.mainRoute); + }); + + debug('adding memoryCache'); + router.use((req, res, next) => + memoryCache(req, res, next, cache, reloadCache) + ); + + return router as Router; +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..ae582cb --- /dev/null +++ b/src/types.ts @@ -0,0 +1,58 @@ +import { Options } from 'express-rate-limit'; +import formidable from 'formidable'; + +export interface MimeOptions { + directory?: string; + rename?: boolean; +} + +export interface MimeImageOptions extends MimeOptions { + compression?: string; + maxHeight?: string; + maxWidth?: string; +} + +export interface UploadConfiguration extends Partial< + Omit +> { + active: boolean; + directory: string; + allowedMimeTypes: string | string[] | Record; + route: string; +} + +export interface QueryParameters { + app: string; + css: string; + following: string; + js: string; + logout: string; + metadata: string; + twt: string; + twts: string; +} + +export interface PostLimiterConfiguration extends Partial { + active: boolean; +} + +export interface TwtKprConfiguration { + accessSecret: string; + mainRoute: string; + privateDirectory: string; + publicDirectory: string; + refreshSecret: string; + twtxtFilename: string; + postLimiterConfiguration?: PostLimiterConfiguration; + queryParameters: QueryParameters; + uploadConfiguration: UploadConfiguration; +} + +export interface TwtKprPluginConfiguration extends Omit< + Partial, + 'postLimiterConfiguration' | 'queryParameters' | 'uploadConfiguration' +> { + postLimiterConfiguration?: Partial; + queryParameters?: Partial; + uploadConfiguration?: Partial; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..944ff0b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "declaration": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "module": "nodenext", + "noImplicitAny": true, + "outDir": "dist", + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "target": "esnext" + }, + "include": ["./package.json", "./types.d.ts", "src/**/*.ts", "test/**/*.ts"], + "exclude": ["node_modules", "./vitest.setup.ts", "**/__tests__/*.*"] +} diff --git a/types.d.ts b/types.d.ts new file mode 100644 index 0000000..4466706 --- /dev/null +++ b/types.d.ts @@ -0,0 +1,9 @@ +import { Request } from "express"; + +declare global { + namespace Express { + interface Request { + username: string; + } + } +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..6592ec6 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,3848 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 8 + cacheKey: 10c0 + +"@cacheable/node-cache@npm:^2.0.2": + version: 2.0.2 + resolution: "@cacheable/node-cache@npm:2.0.2" + dependencies: + "@cacheable/utils": "npm:^2.4.0" + hookified: "npm:^1.15.0" + keyv: "npm:^5.6.0" + checksum: 10c0/bc8cbc06daee03e4da4be7985ed2ea068cbb14602304e251dcb67c71a48de964abfb8b130e11a5560116221aae368679b10944e4292f5df1bdd9ddef8f3d53ee + languageName: node + linkType: hard + +"@cacheable/utils@npm:^2.4.0": + version: 2.4.0 + resolution: "@cacheable/utils@npm:2.4.0" + dependencies: + hashery: "npm:^1.5.0" + keyv: "npm:^5.6.0" + checksum: 10c0/07803578b765a73e177f56086bc3b3cbeec6fcf5f2a1458b0469e63dc95c9afe1ba0a208110e6c7c59bfd8ca4c7ac84a5d8146c1799287bb716073ad8e054062 + languageName: node + linkType: hard + +"@emnapi/core@npm:^1.7.1": + version: 1.9.0 + resolution: "@emnapi/core@npm:1.9.0" + dependencies: + "@emnapi/wasi-threads": "npm:1.2.0" + tslib: "npm:^2.4.0" + checksum: 10c0/defbfa5861aa5ff1346dbc6a19df50d727ae76ae276a31a97b178db8eecae0c5179976878087b43ac2441750e40e6c50e465280383256deb16dd2fb167dd515c + languageName: node + linkType: hard + +"@emnapi/runtime@npm:^1.7.1": + version: 1.9.0 + resolution: "@emnapi/runtime@npm:1.9.0" + dependencies: + tslib: "npm:^2.4.0" + checksum: 10c0/f825e53b2d3f9d31fd880e669197d006bb5158c3a52ab25f0546f3d52ac58eb539a4bd1dcc378af6c10d202956fa064b28ab7b572a76de58972c0b8656a692ef + languageName: node + linkType: hard + +"@emnapi/wasi-threads@npm:1.2.0": + version: 1.2.0 + resolution: "@emnapi/wasi-threads@npm:1.2.0" + dependencies: + tslib: "npm:^2.4.0" + checksum: 10c0/1e3724b5814b06c14782fda87eee9b9aa68af01576c81ffeaefdf621ddb74386e419d5b3b1027b6a8172397729d95a92f814fc4b8d3c224376428faa07a6a01a + languageName: node + linkType: hard + +"@esbuild/aix-ppc64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/aix-ppc64@npm:0.27.3" + conditions: os=aix & cpu=ppc64 + languageName: node + linkType: hard + +"@esbuild/android-arm64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/android-arm64@npm:0.27.3" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/android-arm@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/android-arm@npm:0.27.3" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"@esbuild/android-x64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/android-x64@npm:0.27.3" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/darwin-arm64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/darwin-arm64@npm:0.27.3" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/darwin-x64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/darwin-x64@npm:0.27.3" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/freebsd-arm64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/freebsd-arm64@npm:0.27.3" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/freebsd-x64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/freebsd-x64@npm:0.27.3" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/linux-arm64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/linux-arm64@npm:0.27.3" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/linux-arm@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/linux-arm@npm:0.27.3" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@esbuild/linux-ia32@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/linux-ia32@npm:0.27.3" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + +"@esbuild/linux-loong64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/linux-loong64@npm:0.27.3" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + +"@esbuild/linux-mips64el@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/linux-mips64el@npm:0.27.3" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + +"@esbuild/linux-ppc64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/linux-ppc64@npm:0.27.3" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + +"@esbuild/linux-riscv64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/linux-riscv64@npm:0.27.3" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + +"@esbuild/linux-s390x@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/linux-s390x@npm:0.27.3" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + +"@esbuild/linux-x64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/linux-x64@npm:0.27.3" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/netbsd-arm64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/netbsd-arm64@npm:0.27.3" + conditions: os=netbsd & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/netbsd-x64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/netbsd-x64@npm:0.27.3" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/openbsd-arm64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/openbsd-arm64@npm:0.27.3" + conditions: os=openbsd & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/openbsd-x64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/openbsd-x64@npm:0.27.3" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/openharmony-arm64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/openharmony-arm64@npm:0.27.3" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/sunos-x64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/sunos-x64@npm:0.27.3" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/win32-arm64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/win32-arm64@npm:0.27.3" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/win32-ia32@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/win32-ia32@npm:0.27.3" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@esbuild/win32-x64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/win32-x64@npm:0.27.3" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@eslint-community/eslint-utils@npm:^4.8.0": + version: 4.9.1 + resolution: "@eslint-community/eslint-utils@npm:4.9.1" + dependencies: + eslint-visitor-keys: "npm:^3.4.3" + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + checksum: 10c0/dc4ab5e3e364ef27e33666b11f4b86e1a6c1d7cbf16f0c6ff87b1619b3562335e9201a3d6ce806221887ff780ec9d828962a290bb910759fd40a674686503f02 + languageName: node + linkType: hard + +"@eslint-community/regexpp@npm:^4.12.2": + version: 4.12.2 + resolution: "@eslint-community/regexpp@npm:4.12.2" + checksum: 10c0/fddcbc66851b308478d04e302a4d771d6917a0b3740dc351513c0da9ca2eab8a1adf99f5e0aa7ab8b13fa0df005c81adeee7e63a92f3effd7d367a163b721c2d + languageName: node + linkType: hard + +"@eslint/config-array@npm:^0.23.3": + version: 0.23.3 + resolution: "@eslint/config-array@npm:0.23.3" + dependencies: + "@eslint/object-schema": "npm:^3.0.3" + debug: "npm:^4.3.1" + minimatch: "npm:^10.2.4" + checksum: 10c0/7c19027acf9110cc542513ff9f3ca73a61d127e900c24f0e8e4d5e18aa22baf08d1d5bc386563d2f9311095f3b7898fe9b627b590fe9232b745ef60d4443cf9f + languageName: node + linkType: hard + +"@eslint/config-helpers@npm:^0.5.2": + version: 0.5.2 + resolution: "@eslint/config-helpers@npm:0.5.2" + dependencies: + "@eslint/core": "npm:^1.1.0" + checksum: 10c0/0dc65bc5dd80441afbf5007cae702a5d9dd08893e95fed702a463366cf9ce2f4fd90adb09f9012cb4fcc9783d897ccb739067b1b8a5942f4c8288a6efb396d58 + languageName: node + linkType: hard + +"@eslint/core@npm:^1.1.0": + version: 1.1.0 + resolution: "@eslint/core@npm:1.1.0" + dependencies: + "@types/json-schema": "npm:^7.0.15" + checksum: 10c0/0f875d6f24fbf67cc796e01c2ca82884f755488052ed84183e56377c5b90fe10b491a26e600642db4daea1d5d8ab7906ec12f2bd5cbdb5004b0ef73c802bdb57 + languageName: node + linkType: hard + +"@eslint/core@npm:^1.1.1": + version: 1.1.1 + resolution: "@eslint/core@npm:1.1.1" + dependencies: + "@types/json-schema": "npm:^7.0.15" + checksum: 10c0/129c654c78afc1f6d61dccb0ce841be667f09f052f7d5642614b6ba5eeebd579ca6cc336d7b750d88625e61f7aad22fdd62bf83847fbfc10cc3e58cfe6c5072e + languageName: node + linkType: hard + +"@eslint/object-schema@npm:^3.0.3": + version: 3.0.3 + resolution: "@eslint/object-schema@npm:3.0.3" + checksum: 10c0/4abbb7cba5419dce46ae8aa8e979fa190f2e906a8e1b5a8e22e4489f62a68dea3967679f66acbc0c3ef89f33252a7460e39fc2d6f2b4f616a137f3514eda4784 + languageName: node + linkType: hard + +"@eslint/plugin-kit@npm:^0.6.1": + version: 0.6.1 + resolution: "@eslint/plugin-kit@npm:0.6.1" + dependencies: + "@eslint/core": "npm:^1.1.1" + levn: "npm:^0.4.1" + checksum: 10c0/f8354a7b92cc41e7a55d51986d192134be84f9dc0c91b5e649d075d733b56981c4ca8bf4460d54120c4c87b47984167bad2cb9bceb303f11b0a3bad22b3ed06a + languageName: node + linkType: hard + +"@exodus/blakejs@npm:^1.1.1-exodus.0": + version: 1.1.1-exodus.0 + resolution: "@exodus/blakejs@npm:1.1.1-exodus.0" + checksum: 10c0/abd5ef599324137cddff0c33e67534532444d7031ed895894e1c8424b898aa30ffa1bb775be9daaf3d6dc71e5a2f180694e7f304c8ab876300f1b8338cf07850 + languageName: node + linkType: hard + +"@humanfs/core@npm:^0.19.1": + version: 0.19.1 + resolution: "@humanfs/core@npm:0.19.1" + checksum: 10c0/aa4e0152171c07879b458d0e8a704b8c3a89a8c0541726c6b65b81e84fd8b7564b5d6c633feadc6598307d34564bd53294b533491424e8e313d7ab6c7bc5dc67 + languageName: node + linkType: hard + +"@humanfs/node@npm:^0.16.6": + version: 0.16.7 + resolution: "@humanfs/node@npm:0.16.7" + dependencies: + "@humanfs/core": "npm:^0.19.1" + "@humanwhocodes/retry": "npm:^0.4.0" + checksum: 10c0/9f83d3cf2cfa37383e01e3cdaead11cd426208e04c44adcdd291aa983aaf72d7d3598844d2fe9ce54896bb1bf8bd4b56883376611c8905a19c44684642823f30 + languageName: node + linkType: hard + +"@humanwhocodes/module-importer@npm:^1.0.1": + version: 1.0.1 + resolution: "@humanwhocodes/module-importer@npm:1.0.1" + checksum: 10c0/909b69c3b86d482c26b3359db16e46a32e0fb30bd306a3c176b8313b9e7313dba0f37f519de6aa8b0a1921349e505f259d19475e123182416a506d7f87e7f529 + languageName: node + linkType: hard + +"@humanwhocodes/retry@npm:^0.4.0, @humanwhocodes/retry@npm:^0.4.2": + version: 0.4.3 + resolution: "@humanwhocodes/retry@npm:0.4.3" + checksum: 10c0/3775bb30087d4440b3f7406d5a057777d90e4b9f435af488a4923ef249e93615fb78565a85f173a186a076c7706a81d0d57d563a2624e4de2c5c9c66c486ce42 + languageName: node + linkType: hard + +"@isaacs/fs-minipass@npm:^4.0.0": + version: 4.0.1 + resolution: "@isaacs/fs-minipass@npm:4.0.1" + dependencies: + minipass: "npm:^7.0.4" + checksum: 10c0/c25b6dc1598790d5b55c0947a9b7d111cfa92594db5296c3b907e2f533c033666f692a3939eadac17b1c7c40d362d0b0635dc874cbfe3e70db7c2b07cc97a5d2 + languageName: node + linkType: hard + +"@jridgewell/sourcemap-codec@npm:^1.5.5": + version: 1.5.5 + resolution: "@jridgewell/sourcemap-codec@npm:1.5.5" + checksum: 10c0/f9e538f302b63c0ebc06eecb1dd9918dd4289ed36147a0ddce35d6ea4d7ebbda243cda7b2213b6a5e1d8087a298d5cf630fb2bd39329cdecb82017023f6081a0 + languageName: node + linkType: hard + +"@keyv/serialize@npm:^1.1.1": + version: 1.1.1 + resolution: "@keyv/serialize@npm:1.1.1" + checksum: 10c0/b0008cae4a54400c3abf587b8cc2474c6f528ee58969ce6cf9cb07a04006f80c73c85971d6be6544408318a2bc40108236a19a82aea0a6de95aae49533317374 + languageName: node + linkType: hard + +"@napi-rs/wasm-runtime@npm:^1.1.1": + version: 1.1.1 + resolution: "@napi-rs/wasm-runtime@npm:1.1.1" + dependencies: + "@emnapi/core": "npm:^1.7.1" + "@emnapi/runtime": "npm:^1.7.1" + "@tybys/wasm-util": "npm:^0.10.1" + checksum: 10c0/04d57b67e80736e41fe44674a011878db0a8ad893f4d44abb9d3608debb7c174224cba2796ed5b0c1d367368159f3ca6be45f1c59222f70e32ddc880f803d447 + languageName: node + linkType: hard + +"@noble/hashes@npm:^1.1.5": + version: 1.8.0 + resolution: "@noble/hashes@npm:1.8.0" + checksum: 10c0/06a0b52c81a6fa7f04d67762e08b2c476a00285858150caeaaff4037356dd5e119f45b2a530f638b77a5eeca013168ec1b655db41bae3236cb2e9d511484fc77 + languageName: node + linkType: hard + +"@npmcli/agent@npm:^4.0.0": + version: 4.0.0 + resolution: "@npmcli/agent@npm:4.0.0" + dependencies: + agent-base: "npm:^7.1.0" + http-proxy-agent: "npm:^7.0.0" + https-proxy-agent: "npm:^7.0.1" + lru-cache: "npm:^11.2.1" + socks-proxy-agent: "npm:^8.0.3" + checksum: 10c0/f7b5ce0f3dd42c3f8c6546e8433573d8049f67ef11ec22aa4704bc41483122f68bf97752e06302c455ead667af5cb753e6a09bff06632bc465c1cfd4c4b75a53 + languageName: node + linkType: hard + +"@npmcli/fs@npm:^5.0.0": + version: 5.0.0 + resolution: "@npmcli/fs@npm:5.0.0" + dependencies: + semver: "npm:^7.3.5" + checksum: 10c0/26e376d780f60ff16e874a0ac9bc3399186846baae0b6e1352286385ac134d900cc5dafaded77f38d77f86898fc923ae1cee9d7399f0275b1aa24878915d722b + languageName: node + linkType: hard + +"@oxc-project/runtime@npm:0.115.0": + version: 0.115.0 + resolution: "@oxc-project/runtime@npm:0.115.0" + checksum: 10c0/88905181724fcad06d2852969e706a25a7b6c4fadac22dd6aece24b882a947eda7487451e0824781c9dc87b40b2c6ee582790e47fec5a9ba5d27c6e8c6c35bc1 + languageName: node + linkType: hard + +"@oxc-project/types@npm:=0.115.0": + version: 0.115.0 + resolution: "@oxc-project/types@npm:0.115.0" + checksum: 10c0/47fc31eb3fb3fcf4119955339f92ba2003f9b445834c1a28ed945cd6b9cd833c7ba66fca88aa5277336c2c55df300a593bc67970e544691eceaa486ebe12cb58 + languageName: node + linkType: hard + +"@paralleldrive/cuid2@npm:^2.2.2": + version: 2.3.1 + resolution: "@paralleldrive/cuid2@npm:2.3.1" + dependencies: + "@noble/hashes": "npm:^1.1.5" + checksum: 10c0/6576b73de49d826b0f33cbab88424dec1f6fa454a9e59a7b621f78c2cfdd2e59d7f48175826d698940a717f45eeb5e87a508583a7316e608f6a05a861a40c129 + languageName: node + linkType: hard + +"@pkgr/core@npm:^0.2.9": + version: 0.2.9 + resolution: "@pkgr/core@npm:0.2.9" + checksum: 10c0/ac8e4e8138b1a7a4ac6282873aef7389c352f1f8b577b4850778f5182e4a39a5241facbe48361fec817f56d02b51691b383010843fb08b34a8e8ea3614688fd5 + languageName: node + linkType: hard + +"@rolldown/binding-android-arm64@npm:1.0.0-rc.9": + version: 1.0.0-rc.9 + resolution: "@rolldown/binding-android-arm64@npm:1.0.0-rc.9" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@rolldown/binding-darwin-arm64@npm:1.0.0-rc.9": + version: 1.0.0-rc.9 + resolution: "@rolldown/binding-darwin-arm64@npm:1.0.0-rc.9" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@rolldown/binding-darwin-x64@npm:1.0.0-rc.9": + version: 1.0.0-rc.9 + resolution: "@rolldown/binding-darwin-x64@npm:1.0.0-rc.9" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@rolldown/binding-freebsd-x64@npm:1.0.0-rc.9": + version: 1.0.0-rc.9 + resolution: "@rolldown/binding-freebsd-x64@npm:1.0.0-rc.9" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@rolldown/binding-linux-arm-gnueabihf@npm:1.0.0-rc.9": + version: 1.0.0-rc.9 + resolution: "@rolldown/binding-linux-arm-gnueabihf@npm:1.0.0-rc.9" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@rolldown/binding-linux-arm64-gnu@npm:1.0.0-rc.9": + version: 1.0.0-rc.9 + resolution: "@rolldown/binding-linux-arm64-gnu@npm:1.0.0-rc.9" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@rolldown/binding-linux-arm64-musl@npm:1.0.0-rc.9": + version: 1.0.0-rc.9 + resolution: "@rolldown/binding-linux-arm64-musl@npm:1.0.0-rc.9" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@rolldown/binding-linux-ppc64-gnu@npm:1.0.0-rc.9": + version: 1.0.0-rc.9 + resolution: "@rolldown/binding-linux-ppc64-gnu@npm:1.0.0-rc.9" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + +"@rolldown/binding-linux-s390x-gnu@npm:1.0.0-rc.9": + version: 1.0.0-rc.9 + resolution: "@rolldown/binding-linux-s390x-gnu@npm:1.0.0-rc.9" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + +"@rolldown/binding-linux-x64-gnu@npm:1.0.0-rc.9": + version: 1.0.0-rc.9 + resolution: "@rolldown/binding-linux-x64-gnu@npm:1.0.0-rc.9" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@rolldown/binding-linux-x64-musl@npm:1.0.0-rc.9": + version: 1.0.0-rc.9 + resolution: "@rolldown/binding-linux-x64-musl@npm:1.0.0-rc.9" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@rolldown/binding-openharmony-arm64@npm:1.0.0-rc.9": + version: 1.0.0-rc.9 + resolution: "@rolldown/binding-openharmony-arm64@npm:1.0.0-rc.9" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + +"@rolldown/binding-wasm32-wasi@npm:1.0.0-rc.9": + version: 1.0.0-rc.9 + resolution: "@rolldown/binding-wasm32-wasi@npm:1.0.0-rc.9" + dependencies: + "@napi-rs/wasm-runtime": "npm:^1.1.1" + conditions: cpu=wasm32 + languageName: node + linkType: hard + +"@rolldown/binding-win32-arm64-msvc@npm:1.0.0-rc.9": + version: 1.0.0-rc.9 + resolution: "@rolldown/binding-win32-arm64-msvc@npm:1.0.0-rc.9" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@rolldown/binding-win32-x64-msvc@npm:1.0.0-rc.9": + version: 1.0.0-rc.9 + resolution: "@rolldown/binding-win32-x64-msvc@npm:1.0.0-rc.9" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@rolldown/pluginutils@npm:1.0.0-rc.9": + version: 1.0.0-rc.9 + resolution: "@rolldown/pluginutils@npm:1.0.0-rc.9" + checksum: 10c0/fca488fb96b134ccf95b42632b6112b4abb8b3a9688f166fbd627410def2538ee201953717d234ddecbff62dfe4edc4e72c657b01a9d0750134608d767eea5fd + languageName: node + linkType: hard + +"@standard-schema/spec@npm:^1.1.0": + version: 1.1.0 + resolution: "@standard-schema/spec@npm:1.1.0" + checksum: 10c0/d90f55acde4b2deb983529c87e8025fa693de1a5e8b49ecc6eb84d1fd96328add0e03d7d551442156c7432fd78165b2c26ff561b970a9a881f046abb78d6a526 + languageName: node + linkType: hard + +"@tybys/wasm-util@npm:^0.10.1": + version: 0.10.1 + resolution: "@tybys/wasm-util@npm:0.10.1" + dependencies: + tslib: "npm:^2.4.0" + checksum: 10c0/b255094f293794c6d2289300c5fbcafbb5532a3aed3a5ffd2f8dc1828e639b88d75f6a376dd8f94347a44813fd7a7149d8463477a9a49525c8b2dcaa38c2d1e8 + languageName: node + linkType: hard + +"@types/body-parser@npm:*": + version: 1.19.6 + resolution: "@types/body-parser@npm:1.19.6" + dependencies: + "@types/connect": "npm:*" + "@types/node": "npm:*" + checksum: 10c0/542da05c924dce58ee23f50a8b981fee36921850c82222e384931fda3e106f750f7880c47be665217d72dbe445129049db6eb1f44e7a06b09d62af8f3cca8ea7 + languageName: node + linkType: hard + +"@types/chai@npm:^5.2.2": + version: 5.2.3 + resolution: "@types/chai@npm:5.2.3" + dependencies: + "@types/deep-eql": "npm:*" + assertion-error: "npm:^2.0.1" + checksum: 10c0/e0ef1de3b6f8045a5e473e867c8565788c444271409d155588504840ad1a53611011f85072188c2833941189400228c1745d78323dac13fcede9c2b28bacfb2f + languageName: node + linkType: hard + +"@types/connect@npm:*": + version: 3.4.38 + resolution: "@types/connect@npm:3.4.38" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/2e1cdba2c410f25649e77856505cd60223250fa12dff7a503e492208dbfdd25f62859918f28aba95315251fd1f5e1ffbfca1e25e73037189ab85dd3f8d0a148c + languageName: node + linkType: hard + +"@types/cookie-parser@npm:^1.4.10": + version: 1.4.10 + resolution: "@types/cookie-parser@npm:1.4.10" + peerDependencies: + "@types/express": "*" + checksum: 10c0/5e2c0e0fd9cb47eae5bd7cd73768b36b7711594cbd07969a03f015118d57b832291726d9cb7bd350c9e3a1707e5e2f082e7e0635e8ce335474897ab940d55771 + languageName: node + linkType: hard + +"@types/cookiejar@npm:^2.1.5": + version: 2.1.5 + resolution: "@types/cookiejar@npm:2.1.5" + checksum: 10c0/af38c3d84aebb3ccc6e46fb6afeeaac80fb26e63a487dd4db5a8b87e6ad3d4b845ba1116b2ae90d6f886290a36200fa433d8b1f6fe19c47da6b81872ce9a2764 + languageName: node + linkType: hard + +"@types/cors@npm:^2.8.19": + version: 2.8.19 + resolution: "@types/cors@npm:2.8.19" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/b5dd407040db7d8aa1bd36e79e5f3f32292f6b075abc287529e9f48df1a25fda3e3799ba30b4656667ffb931d3b75690c1d6ca71e39f7337ea6dfda8581916d0 + languageName: node + linkType: hard + +"@types/debug@npm:^4.1.12": + version: 4.1.12 + resolution: "@types/debug@npm:4.1.12" + dependencies: + "@types/ms": "npm:*" + checksum: 10c0/5dcd465edbb5a7f226e9a5efd1f399c6172407ef5840686b73e3608ce135eeca54ae8037dcd9f16bdb2768ac74925b820a8b9ecc588a58ca09eca6acabe33e2f + languageName: node + linkType: hard + +"@types/deep-eql@npm:*": + version: 4.0.2 + resolution: "@types/deep-eql@npm:4.0.2" + checksum: 10c0/bf3f811843117900d7084b9d0c852da9a044d12eb40e6de73b552598a6843c21291a8a381b0532644574beecd5e3491c5ff3a0365ab86b15d59862c025384844 + languageName: node + linkType: hard + +"@types/esrecurse@npm:^4.3.1": + version: 4.3.1 + resolution: "@types/esrecurse@npm:4.3.1" + checksum: 10c0/90dad74d5da3ad27606d8e8e757322f33171cfeaa15ad558b615cf71bb2a516492d18f55f4816384685a3eb2412142e732bbae9a4a7cd2cf3deb7572aa4ebe03 + languageName: node + linkType: hard + +"@types/estree@npm:^1.0.0, @types/estree@npm:^1.0.6, @types/estree@npm:^1.0.8": + version: 1.0.8 + resolution: "@types/estree@npm:1.0.8" + checksum: 10c0/39d34d1afaa338ab9763f37ad6066e3f349444f9052b9676a7cc0252ef9485a41c6d81c9c4e0d26e9077993354edf25efc853f3224dd4b447175ef62bdcc86a5 + languageName: node + linkType: hard + +"@types/express-serve-static-core@npm:^5.0.0": + version: 5.1.1 + resolution: "@types/express-serve-static-core@npm:5.1.1" + dependencies: + "@types/node": "npm:*" + "@types/qs": "npm:*" + "@types/range-parser": "npm:*" + "@types/send": "npm:*" + checksum: 10c0/ee88216e114368ef06bcafeceb74a7e8671b90900fb0ab1d49ff41542c3a344231ef0d922bf63daa79f0585f3eebe2ce5ec7f83facc581eff8bcdb136a225ef3 + languageName: node + linkType: hard + +"@types/express-session@npm:^1.18.2": + version: 1.18.2 + resolution: "@types/express-session@npm:1.18.2" + dependencies: + "@types/express": "npm:*" + checksum: 10c0/5d5aa134ce8990920b35f2dd0aa55168af44faaf14789b6921d361ce016c43bdc66feba287753981a2fee33fd95b8a829c4418c3ca480b03961724b8bc13e453 + languageName: node + linkType: hard + +"@types/express@npm:*, @types/express@npm:^5.0.6": + version: 5.0.6 + resolution: "@types/express@npm:5.0.6" + dependencies: + "@types/body-parser": "npm:*" + "@types/express-serve-static-core": "npm:^5.0.0" + "@types/serve-static": "npm:^2" + checksum: 10c0/f1071e3389a955d4f9a38aae38634121c7cd9b3171ba4201ec9b56bd534aba07866839d278adc0dda05b942b05a901a02fd174201c3b1f70ce22b10b6c68f24b + languageName: node + linkType: hard + +"@types/formidable@npm:^3.5.0": + version: 3.5.0 + resolution: "@types/formidable@npm:3.5.0" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/6e6b70a6f80f29f053a5e11a1277ce23c5491079897cb9b1c2c1cf403f6b0590ab1bd8f7fce45bddb14366414e3291c8b8ab1ccb688237448e350effb939c4f3 + languageName: node + linkType: hard + +"@types/http-errors@npm:*": + version: 2.0.5 + resolution: "@types/http-errors@npm:2.0.5" + checksum: 10c0/00f8140fbc504f47356512bd88e1910c2f07e04233d99c88c854b3600ce0523c8cd0ba7d1897667243282eb44c59abb9245959e2428b9de004f93937f52f7c15 + languageName: node + linkType: hard + +"@types/json-schema@npm:^7.0.15": + version: 7.0.15 + resolution: "@types/json-schema@npm:7.0.15" + checksum: 10c0/a996a745e6c5d60292f36731dd41341339d4eeed8180bb09226e5c8d23759067692b1d88e5d91d72ee83dfc00d3aca8e7bd43ea120516c17922cbcb7c3e252db + languageName: node + linkType: hard + +"@types/jsonwebtoken@npm:^9.0.10": + version: 9.0.10 + resolution: "@types/jsonwebtoken@npm:9.0.10" + dependencies: + "@types/ms": "npm:*" + "@types/node": "npm:*" + checksum: 10c0/0688ac8fb75f809201cb7e18a12b9d80ce539cb9dd27e1b01e11807cb1a337059e899b8ee3abc3f2c9417f02e363a3069d9eab9ef9724b1da1f0e10713514f94 + languageName: node + linkType: hard + +"@types/methods@npm:^1.1.4": + version: 1.1.4 + resolution: "@types/methods@npm:1.1.4" + checksum: 10c0/a78534d79c300718298bfff92facd07bf38429c66191f640c1db4c9cff1e36f819304298a96f7536b6512bfc398e5c3e6b831405e138cd774b88ad7be78d682a + languageName: node + linkType: hard + +"@types/morgan@npm:^1.9.10": + version: 1.9.10 + resolution: "@types/morgan@npm:1.9.10" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/419f3fdefd89771f4935b690d7e49954ef8e1e3dd4a050a4b53daa363911df987c14e6d5f4fcba07a931a32c5a6ef1b318b6927c1b390cfb8ec900e84a230c02 + languageName: node + linkType: hard + +"@types/ms@npm:*": + version: 2.1.0 + resolution: "@types/ms@npm:2.1.0" + checksum: 10c0/5ce692ffe1549e1b827d99ef8ff71187457e0eb44adbae38fdf7b9a74bae8d20642ee963c14516db1d35fa2652e65f47680fdf679dcbde52bbfadd021f497225 + languageName: node + linkType: hard + +"@types/node@npm:*": + version: 25.3.0 + resolution: "@types/node@npm:25.3.0" + dependencies: + undici-types: "npm:~7.18.0" + checksum: 10c0/7b2b18c9d68047157367fc2f786d4f166d22dc0ad9f82331ca02fb16f2f391854123dbe604dcb938cda119c87051e4bb71dcb9ece44a579f483a6f96d4bd41de + languageName: node + linkType: hard + +"@types/node@npm:^25.5.0": + version: 25.5.0 + resolution: "@types/node@npm:25.5.0" + dependencies: + undici-types: "npm:~7.18.0" + checksum: 10c0/70c508165b6758c4f88d4f91abca526c3985eee1985503d4c2bd994dbaf588e52ac57e571160f18f117d76e963570ac82bd20e743c18987e82564312b3b62119 + languageName: node + linkType: hard + +"@types/qs@npm:*": + version: 6.14.0 + resolution: "@types/qs@npm:6.14.0" + checksum: 10c0/5b3036df6e507483869cdb3858201b2e0b64b4793dc4974f188caa5b5732f2333ab9db45c08157975054d3b070788b35088b4bc60257ae263885016ee2131310 + languageName: node + linkType: hard + +"@types/range-parser@npm:*": + version: 1.2.7 + resolution: "@types/range-parser@npm:1.2.7" + checksum: 10c0/361bb3e964ec5133fa40644a0b942279ed5df1949f21321d77de79f48b728d39253e5ce0408c9c17e4e0fd95ca7899da36841686393b9f7a1e209916e9381a3c + languageName: node + linkType: hard + +"@types/send@npm:*": + version: 1.2.1 + resolution: "@types/send@npm:1.2.1" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/7673747f8c2d8e67f3b1b3b57e9d4d681801a4f7b526ecf09987bb9a84a61cf94aa411c736183884dc762c1c402a61681eb1ef200d8d45d7e5ec0ab67ea5f6c1 + languageName: node + linkType: hard + +"@types/serve-static@npm:^2": + version: 2.2.0 + resolution: "@types/serve-static@npm:2.2.0" + dependencies: + "@types/http-errors": "npm:*" + "@types/node": "npm:*" + checksum: 10c0/a3c6126bdbf9685e6c7dc03ad34639666eff32754e912adeed9643bf3dd3aa0ff043002a7f69039306e310d233eb8e160c59308f95b0a619f32366bbc48ee094 + languageName: node + linkType: hard + +"@types/superagent@npm:^8.1.0": + version: 8.1.9 + resolution: "@types/superagent@npm:8.1.9" + dependencies: + "@types/cookiejar": "npm:^2.1.5" + "@types/methods": "npm:^1.1.4" + "@types/node": "npm:*" + form-data: "npm:^4.0.0" + checksum: 10c0/12631f1d8b3a62e1f435bc885f6d64d1a2d1ae82b80f0c6d63d4d6372c40b6f1fee6b3da59ac18bb86250b1eb73583bf2d4b1f7882048c32468791c560c69b7c + languageName: node + linkType: hard + +"@types/supertest@npm:^7.2.0": + version: 7.2.0 + resolution: "@types/supertest@npm:7.2.0" + dependencies: + "@types/methods": "npm:^1.1.4" + "@types/superagent": "npm:^8.1.0" + checksum: 10c0/78c33e968acd45207acdd965ccbd5eb7a279813ff68fab1acc438937ed017698102cc077cef8aa60ec6caefff2fa61171e902eab40607fd7ce82ead3a82b766e + languageName: node + linkType: hard + +"@vitest/expect@npm:4.1.0": + version: 4.1.0 + resolution: "@vitest/expect@npm:4.1.0" + dependencies: + "@standard-schema/spec": "npm:^1.1.0" + "@types/chai": "npm:^5.2.2" + "@vitest/spy": "npm:4.1.0" + "@vitest/utils": "npm:4.1.0" + chai: "npm:^6.2.2" + tinyrainbow: "npm:^3.0.3" + checksum: 10c0/91cd7bb036401df5dfd9204f3de9a0afdb21dea6ee154622e5ed849e87a0df68b74258d490559c7046d3c03bc7aa634e9b0c166942a21d5e475c86c971486091 + languageName: node + linkType: hard + +"@vitest/mocker@npm:4.1.0": + version: 4.1.0 + resolution: "@vitest/mocker@npm:4.1.0" + dependencies: + "@vitest/spy": "npm:4.1.0" + estree-walker: "npm:^3.0.3" + magic-string: "npm:^0.30.21" + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + checksum: 10c0/f61d3df6461008eb1e62ba465172207b29bd0d9866ff6bc88cd40fc99cd5d215ad89e2894ba6de87068e33f75de903b28a65ccc6074edf3de1fbead6a4a369cc + languageName: node + linkType: hard + +"@vitest/pretty-format@npm:4.1.0": + version: 4.1.0 + resolution: "@vitest/pretty-format@npm:4.1.0" + dependencies: + tinyrainbow: "npm:^3.0.3" + checksum: 10c0/638077f53b5f24ff2d4bc062e69931fa718141db28ddafe435de98a402586b82e8c3cadfc580c0ad233d7f0203aa22d866ac2adca98b83038dbd5423c3d7fe27 + languageName: node + linkType: hard + +"@vitest/runner@npm:4.1.0": + version: 4.1.0 + resolution: "@vitest/runner@npm:4.1.0" + dependencies: + "@vitest/utils": "npm:4.1.0" + pathe: "npm:^2.0.3" + checksum: 10c0/9e09ca1b9070d3fe26c9bd48443d21b9fe2cb9abb2f694300bd9e5065f4e904f7322c07cd4bafadfed6fb11adfb50e4d1535f327ac6d24b6c373e92be90510bc + languageName: node + linkType: hard + +"@vitest/snapshot@npm:4.1.0": + version: 4.1.0 + resolution: "@vitest/snapshot@npm:4.1.0" + dependencies: + "@vitest/pretty-format": "npm:4.1.0" + "@vitest/utils": "npm:4.1.0" + magic-string: "npm:^0.30.21" + pathe: "npm:^2.0.3" + checksum: 10c0/582c22988c47a99d93dd17ef660427fefe101f67ae4394b64fe58ec103ddc55fc5993626b4a2b556e0a38d40552abaca78196907455e794805ba197b3d56860f + languageName: node + linkType: hard + +"@vitest/spy@npm:4.1.0": + version: 4.1.0 + resolution: "@vitest/spy@npm:4.1.0" + checksum: 10c0/363776bbffda45af76ff500deacb9b1a35ad8b889462c1be9ebe5f29578ce1dd2c4bd7858c8188614a7db9699a5c802b7beb72e5a18ab5130a70326817961446 + languageName: node + linkType: hard + +"@vitest/utils@npm:4.1.0": + version: 4.1.0 + resolution: "@vitest/utils@npm:4.1.0" + dependencies: + "@vitest/pretty-format": "npm:4.1.0" + convert-source-map: "npm:^2.0.0" + tinyrainbow: "npm:^3.0.3" + checksum: 10c0/222afbdef4f680a554bb6c3d946a4a879a441ebfb8597295cb7554d295e0e2624f3d4c2920b5767bbb8961a9f8a16756270ffc84032f5ea432cdce080ccab050 + languageName: node + linkType: hard + +"abbrev@npm:^4.0.0": + version: 4.0.0 + resolution: "abbrev@npm:4.0.0" + checksum: 10c0/b4cc16935235e80702fc90192e349e32f8ef0ed151ef506aa78c81a7c455ec18375c4125414b99f84b2e055199d66383e787675f0bcd87da7a4dbd59f9eac1d5 + languageName: node + linkType: hard + +"accepts@npm:^2.0.0": + version: 2.0.0 + resolution: "accepts@npm:2.0.0" + dependencies: + mime-types: "npm:^3.0.0" + negotiator: "npm:^1.0.0" + checksum: 10c0/98374742097e140891546076215f90c32644feacf652db48412329de4c2a529178a81aa500fbb13dd3e6cbf6e68d829037b123ac037fc9a08bcec4b87b358eef + languageName: node + linkType: hard + +"acorn-jsx@npm:^5.3.2": + version: 5.3.2 + resolution: "acorn-jsx@npm:5.3.2" + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + checksum: 10c0/4c54868fbef3b8d58927d5e33f0a4de35f59012fe7b12cf9dfbb345fb8f46607709e1c4431be869a23fb63c151033d84c4198fa9f79385cec34fcb1dd53974c1 + languageName: node + linkType: hard + +"acorn@npm:^8.16.0": + version: 8.16.0 + resolution: "acorn@npm:8.16.0" + bin: + acorn: bin/acorn + checksum: 10c0/c9c52697227661b68d0debaf972222d4f622aa06b185824164e153438afa7b08273432ca43ea792cadb24dada1d46f6f6bb1ef8de9956979288cc1b96bf9914e + languageName: node + linkType: hard + +"agent-base@npm:^7.1.0, agent-base@npm:^7.1.2": + version: 7.1.4 + resolution: "agent-base@npm:7.1.4" + checksum: 10c0/c2c9ab7599692d594b6a161559ada307b7a624fa4c7b03e3afdb5a5e31cd0e53269115b620fcab024c5ac6a6f37fa5eb2e004f076ad30f5f7e6b8b671f7b35fe + languageName: node + linkType: hard + +"ajv@npm:^6.14.0": + version: 6.14.0 + resolution: "ajv@npm:6.14.0" + dependencies: + fast-deep-equal: "npm:^3.1.1" + fast-json-stable-stringify: "npm:^2.0.0" + json-schema-traverse: "npm:^0.4.1" + uri-js: "npm:^4.2.2" + checksum: 10c0/a2bc39b0555dc9802c899f86990eb8eed6e366cddbf65be43d5aa7e4f3c4e1a199d5460fd7ca4fb3d864000dbbc049253b72faa83b3b30e641ca52cb29a68c22 + languageName: node + linkType: hard + +"asap@npm:^2.0.0": + version: 2.0.6 + resolution: "asap@npm:2.0.6" + checksum: 10c0/c6d5e39fe1f15e4b87677460bd66b66050cd14c772269cee6688824c1410a08ab20254bb6784f9afb75af9144a9f9a7692d49547f4d19d715aeb7c0318f3136d + languageName: node + linkType: hard + +"asn1.js@npm:^5.4.1": + version: 5.4.1 + resolution: "asn1.js@npm:5.4.1" + dependencies: + bn.js: "npm:^4.0.0" + inherits: "npm:^2.0.1" + minimalistic-assert: "npm:^1.0.0" + safer-buffer: "npm:^2.1.0" + checksum: 10c0/b577232fa6069cc52bb128e564002c62b2b1fe47f7137bdcd709c0b8495aa79cee0f8cc458a831b2d8675900eea0d05781b006be5e1aa4f0ae3577a73ec20324 + languageName: node + linkType: hard + +"assertion-error@npm:^2.0.1": + version: 2.0.1 + resolution: "assertion-error@npm:2.0.1" + checksum: 10c0/bbbcb117ac6480138f8c93cf7f535614282dea9dc828f540cdece85e3c665e8f78958b96afac52f29ff883c72638e6a87d469ecc9fe5bc902df03ed24a55dba8 + languageName: node + linkType: hard + +"async-function@npm:^1.0.0": + version: 1.0.0 + resolution: "async-function@npm:1.0.0" + checksum: 10c0/669a32c2cb7e45091330c680e92eaeb791bc1d4132d827591e499cd1f776ff5a873e77e5f92d0ce795a8d60f10761dec9ddfe7225a5de680f5d357f67b1aac73 + languageName: node + linkType: hard + +"async-generator-function@npm:^1.0.0": + version: 1.0.0 + resolution: "async-generator-function@npm:1.0.0" + checksum: 10c0/2c50ef856c543ad500d8d8777d347e3c1ba623b93e99c9263ecc5f965c1b12d2a140e2ab6e43c3d0b85366110696f28114649411cbcd10b452a92a2318394186 + languageName: node + linkType: hard + +"asynckit@npm:^0.4.0": + version: 0.4.0 + resolution: "asynckit@npm:0.4.0" + checksum: 10c0/d73e2ddf20c4eb9337e1b3df1a0f6159481050a5de457c55b14ea2e5cb6d90bb69e004c9af54737a5ee0917fcf2c9e25de67777bbe58261847846066ba75bc9d + languageName: node + linkType: hard + +"bagpipe@npm:^0.3.5": + version: 0.3.5 + resolution: "bagpipe@npm:0.3.5" + checksum: 10c0/0fc6d3bb283ac93b00b943e8aa541ccb1e3a17a627ca25d72585ef18a286558861d41662c62e4040b9f5fe93ad927aa6cd5153798971e6ebf1a9e748da25aee1 + languageName: node + linkType: hard + +"balanced-match@npm:^4.0.2": + version: 4.0.4 + resolution: "balanced-match@npm:4.0.4" + checksum: 10c0/07e86102a3eb2ee2a6a1a89164f29d0dbaebd28f2ca3f5ca786f36b8b23d9e417eb3be45a4acf754f837be5ac0a2317de90d3fcb7f4f4dc95720a1f36b26a17b + languageName: node + linkType: hard + +"base32.js@npm:^0.1.0": + version: 0.1.0 + resolution: "base32.js@npm:0.1.0" + checksum: 10c0/d5f520f8082193850df798a0a64c528eb56b25d507daa94393c39342cc07c41e1e8e3fae71dc318e397b9e28cee800548f582493018d46962b20fce8735efaad + languageName: node + linkType: hard + +"bcryptjs@npm:^3.0.3": + version: 3.0.3 + resolution: "bcryptjs@npm:3.0.3" + bin: + bcrypt: bin/bcrypt + checksum: 10c0/127c94699f07eab5fbbb89d600c3f0e36ae6fd001e028139740b41fffb62a23a2743523fb88f442ae3308a27d4843b97ccb9e436d78f5c5899be187d0972c6d5 + languageName: node + linkType: hard + +"bn.js@npm:^4.0.0": + version: 4.12.3 + resolution: "bn.js@npm:4.12.3" + checksum: 10c0/53b6a4db8a583abd2522eacd480fece26fe6c4d8d35d03e5e11e15cb0873a3044eb4e3d1f9fef56f47eb008219e99ba5b620c26f57db49a687c6ab2cf848d50b + languageName: node + linkType: hard + +"body-parser@npm:^2.2.1": + version: 2.2.2 + resolution: "body-parser@npm:2.2.2" + dependencies: + bytes: "npm:^3.1.2" + content-type: "npm:^1.0.5" + debug: "npm:^4.4.3" + http-errors: "npm:^2.0.0" + iconv-lite: "npm:^0.7.0" + on-finished: "npm:^2.4.1" + qs: "npm:^6.14.1" + raw-body: "npm:^3.0.1" + type-is: "npm:^2.0.1" + checksum: 10c0/95a830a003b38654b75166ca765358aa92ee3d561bf0e41d6ccdde0e1a0c9783cab6b90b20eb635d23172c010b59d3563a137a738e74da4ba714463510d05137 + languageName: node + linkType: hard + +"brace-expansion@npm:^5.0.2": + version: 5.0.3 + resolution: "brace-expansion@npm:5.0.3" + dependencies: + balanced-match: "npm:^4.0.2" + checksum: 10c0/e474d300e581ec56851b3863ff1cf18573170c6d06deb199ccbd03b2119c36975f6ce2abc7b770f5bebddc1ab022661a9fea9b4d56f33315d7bef54d8793869e + languageName: node + linkType: hard + +"buffer-equal-constant-time@npm:^1.0.1": + version: 1.0.1 + resolution: "buffer-equal-constant-time@npm:1.0.1" + checksum: 10c0/fb2294e64d23c573d0dd1f1e7a466c3e978fe94a4e0f8183937912ca374619773bef8e2aceb854129d2efecbbc515bbd0cc78d2734a3e3031edb0888531bbc8e + languageName: node + linkType: hard + +"bytes@npm:^3.1.2, bytes@npm:~3.1.2": + version: 3.1.2 + resolution: "bytes@npm:3.1.2" + checksum: 10c0/76d1c43cbd602794ad8ad2ae94095cddeb1de78c5dddaa7005c51af10b0176c69971a6d88e805a90c2b6550d76636e43c40d8427a808b8645ede885de4a0358e + languageName: node + linkType: hard + +"cacache@npm:^20.0.1": + version: 20.0.3 + resolution: "cacache@npm:20.0.3" + dependencies: + "@npmcli/fs": "npm:^5.0.0" + fs-minipass: "npm:^3.0.0" + glob: "npm:^13.0.0" + lru-cache: "npm:^11.1.0" + minipass: "npm:^7.0.3" + minipass-collect: "npm:^2.0.1" + minipass-flush: "npm:^1.0.5" + minipass-pipeline: "npm:^1.2.4" + p-map: "npm:^7.0.2" + ssri: "npm:^13.0.0" + unique-filename: "npm:^5.0.0" + checksum: 10c0/c7da1ca694d20e8f8aedabd21dc11518f809a7d2b59aa76a1fc655db5a9e62379e465c157ddd2afe34b19230808882288effa6911b2de26a088a6d5645123462 + languageName: node + linkType: hard + +"call-bind-apply-helpers@npm:^1.0.1, call-bind-apply-helpers@npm:^1.0.2": + version: 1.0.2 + resolution: "call-bind-apply-helpers@npm:1.0.2" + dependencies: + es-errors: "npm:^1.3.0" + function-bind: "npm:^1.1.2" + checksum: 10c0/47bd9901d57b857590431243fea704ff18078b16890a6b3e021e12d279bbf211d039155e27d7566b374d49ee1f8189344bac9833dec7a20cdec370506361c938 + languageName: node + linkType: hard + +"call-bound@npm:^1.0.2": + version: 1.0.4 + resolution: "call-bound@npm:1.0.4" + dependencies: + call-bind-apply-helpers: "npm:^1.0.2" + get-intrinsic: "npm:^1.3.0" + checksum: 10c0/f4796a6a0941e71c766aea672f63b72bc61234c4f4964dc6d7606e3664c307e7d77845328a8f3359ce39ddb377fed67318f9ee203dea1d47e46165dcf2917644 + languageName: node + linkType: hard + +"chai@npm:^6.2.2": + version: 6.2.2 + resolution: "chai@npm:6.2.2" + checksum: 10c0/e6c69e5f0c11dffe6ea13d0290936ebb68fcc1ad688b8e952e131df6a6d5797d5e860bc55cef1aca2e950c3e1f96daf79e9d5a70fb7dbaab4e46355e2635ed53 + languageName: node + linkType: hard + +"chownr@npm:^3.0.0": + version: 3.0.0 + resolution: "chownr@npm:3.0.0" + checksum: 10c0/43925b87700f7e3893296c8e9c56cc58f926411cce3a6e5898136daaf08f08b9a8eb76d37d3267e707d0dcc17aed2e2ebdf5848c0c3ce95cf910a919935c1b10 + languageName: node + linkType: hard + +"combined-stream@npm:^1.0.8": + version: 1.0.8 + resolution: "combined-stream@npm:1.0.8" + dependencies: + delayed-stream: "npm:~1.0.0" + checksum: 10c0/0dbb829577e1b1e839fa82b40c07ffaf7de8a09b935cadd355a73652ae70a88b4320db322f6634a4ad93424292fa80973ac6480986247f1734a1137debf271d5 + languageName: node + linkType: hard + +"component-emitter@npm:^1.3.1": + version: 1.3.1 + resolution: "component-emitter@npm:1.3.1" + checksum: 10c0/e4900b1b790b5e76b8d71b328da41482118c0f3523a516a41be598dc2785a07fd721098d9bf6e22d89b19f4fa4e1025160dc00317ea111633a3e4f75c2b86032 + languageName: node + linkType: hard + +"content-disposition@npm:^1.0.0": + version: 1.0.1 + resolution: "content-disposition@npm:1.0.1" + checksum: 10c0/bd7ff1fe8d2542d3a2b9a29428cc3591f6ac27bb5595bba2c69664408a68f9538b14cbd92479796ea835b317a09a527c8c7209c4200381dedb0c34d3b658849e + languageName: node + linkType: hard + +"content-type@npm:^1.0.5": + version: 1.0.5 + resolution: "content-type@npm:1.0.5" + checksum: 10c0/b76ebed15c000aee4678c3707e0860cb6abd4e680a598c0a26e17f0bfae723ec9cc2802f0ff1bc6e4d80603719010431d2231018373d4dde10f9ccff9dadf5af + languageName: node + linkType: hard + +"convert-source-map@npm:^2.0.0": + version: 2.0.0 + resolution: "convert-source-map@npm:2.0.0" + checksum: 10c0/8f2f7a27a1a011cc6cc88cc4da2d7d0cfa5ee0369508baae3d98c260bb3ac520691464e5bbe4ae7cdf09860c1d69ecc6f70c63c6e7c7f7e3f18ec08484dc7d9b + languageName: node + linkType: hard + +"cookie-parser@npm:^1.4.7": + version: 1.4.7 + resolution: "cookie-parser@npm:1.4.7" + dependencies: + cookie: "npm:0.7.2" + cookie-signature: "npm:1.0.6" + checksum: 10c0/46bef553de409031b69a6074ce512d131a98e4fa12612669f1a9c3dd98d56897a31db86a3f4338d4a3a895c6f8d5cfd6fa4d99cdf588e0e8eda655efc3f384dc + languageName: node + linkType: hard + +"cookie-signature@npm:1.0.6": + version: 1.0.6 + resolution: "cookie-signature@npm:1.0.6" + checksum: 10c0/b36fd0d4e3fef8456915fcf7742e58fbfcc12a17a018e0eb9501c9d5ef6893b596466f03b0564b81af29ff2538fd0aa4b9d54fe5ccbfb4c90ea50ad29fe2d221 + languageName: node + linkType: hard + +"cookie-signature@npm:^1.2.1, cookie-signature@npm:^1.2.2": + version: 1.2.2 + resolution: "cookie-signature@npm:1.2.2" + checksum: 10c0/54e05df1a293b3ce81589b27dddc445f462f6fa6812147c033350cd3561a42bc14481674e05ed14c7bd0ce1e8bb3dc0e40851bad75415733711294ddce0b7bc6 + languageName: node + linkType: hard + +"cookie-signature@npm:~1.0.7": + version: 1.0.7 + resolution: "cookie-signature@npm:1.0.7" + checksum: 10c0/e7731ad2995ae2efeed6435ec1e22cdd21afef29d300c27281438b1eab2bae04ef0d1a203928c0afec2cee72aa36540b8747406ebe308ad23c8e8cc3c26c9c51 + languageName: node + linkType: hard + +"cookie@npm:0.7.2, cookie@npm:^0.7.1, cookie@npm:~0.7.2": + version: 0.7.2 + resolution: "cookie@npm:0.7.2" + checksum: 10c0/9596e8ccdbf1a3a88ae02cf5ee80c1c50959423e1022e4e60b91dd87c622af1da309253d8abdb258fb5e3eacb4f08e579dc58b4897b8087574eee0fd35dfa5d2 + languageName: node + linkType: hard + +"cookiejar@npm:^2.1.4": + version: 2.1.4 + resolution: "cookiejar@npm:2.1.4" + checksum: 10c0/2dae55611c6e1678f34d93984cbd4bda58f4fe3e5247cc4993f4a305cd19c913bbaf325086ed952e892108115073a747596453d3dc1c34947f47f731818b8ad1 + languageName: node + linkType: hard + +"cross-spawn@npm:^7.0.6": + version: 7.0.6 + resolution: "cross-spawn@npm:7.0.6" + dependencies: + path-key: "npm:^3.1.0" + shebang-command: "npm:^2.0.0" + which: "npm:^2.0.1" + checksum: 10c0/053ea8b2135caff68a9e81470e845613e374e7309a47731e81639de3eaeb90c3d01af0e0b44d2ab9d50b43467223b88567dfeb3262db942dc063b9976718ffc1 + languageName: node + linkType: hard + +"dayjs@npm:^1.11.19": + version: 1.11.19 + resolution: "dayjs@npm:1.11.19" + checksum: 10c0/7d8a6074a343f821f81ea284d700bd34ea6c7abbe8d93bce7aba818948957c1b7f56131702e5e890a5622cdfc05dcebe8aed0b8313bdc6838a594d7846b0b000 + languageName: node + linkType: hard + +"dayjs@npm:^1.11.20": + version: 1.11.20 + resolution: "dayjs@npm:1.11.20" + checksum: 10c0/8af525e2aa100c8db9923d706c42b2b2d30579faf89456619413a5c10916efc92c2b166e193c27c02eb3174b30aa440ee1e7b72b0a2876b3da651d204db848a0 + languageName: node + linkType: hard + +"debug@npm:4, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.7, debug@npm:^4.4.0, debug@npm:^4.4.3": + version: 4.4.3 + resolution: "debug@npm:4.4.3" + dependencies: + ms: "npm:^2.1.3" + peerDependenciesMeta: + supports-color: + optional: true + checksum: 10c0/d79136ec6c83ecbefd0f6a5593da6a9c91ec4d7ddc4b54c883d6e71ec9accb5f67a1a5e96d00a328196b5b5c86d365e98d8a3a70856aaf16b4e7b1985e67f5a6 + languageName: node + linkType: hard + +"debug@npm:~2.6.9": + version: 2.6.9 + resolution: "debug@npm:2.6.9" + dependencies: + ms: "npm:2.0.0" + checksum: 10c0/121908fb839f7801180b69a7e218a40b5a0b718813b886b7d6bdb82001b931c938e2941d1e4450f33a1b1df1da653f5f7a0440c197f29fbf8a6e9d45ff6ef589 + languageName: node + linkType: hard + +"deep-is@npm:^0.1.3": + version: 0.1.4 + resolution: "deep-is@npm:0.1.4" + checksum: 10c0/7f0ee496e0dff14a573dc6127f14c95061b448b87b995fc96c017ce0a1e66af1675e73f1d6064407975bc4ea6ab679497a29fff7b5b9c4e99cb10797c1ad0b4c + languageName: node + linkType: hard + +"delayed-stream@npm:~1.0.0": + version: 1.0.0 + resolution: "delayed-stream@npm:1.0.0" + checksum: 10c0/d758899da03392e6712f042bec80aa293bbe9e9ff1b2634baae6a360113e708b91326594c8a486d475c69d6259afb7efacdc3537bfcda1c6c648e390ce601b19 + languageName: node + linkType: hard + +"depd@npm:^2.0.0, depd@npm:~2.0.0": + version: 2.0.0 + resolution: "depd@npm:2.0.0" + checksum: 10c0/58bd06ec20e19529b06f7ad07ddab60e504d9e0faca4bd23079fac2d279c3594334d736508dc350e06e510aba5e22e4594483b3a6562ce7c17dd797f4cc4ad2c + languageName: node + linkType: hard + +"detect-libc@npm:^2.0.3": + version: 2.1.2 + resolution: "detect-libc@npm:2.1.2" + checksum: 10c0/acc675c29a5649fa1fb6e255f993b8ee829e510b6b56b0910666949c80c364738833417d0edb5f90e4e46be17228b0f2b66a010513984e18b15deeeac49369c4 + languageName: node + linkType: hard + +"dezalgo@npm:^1.0.4": + version: 1.0.4 + resolution: "dezalgo@npm:1.0.4" + dependencies: + asap: "npm:^2.0.0" + wrappy: "npm:1" + checksum: 10c0/8a870ed42eade9a397e6141fe5c025148a59ed52f1f28b1db5de216b4d57f0af7a257070c3af7ce3d5508c1ce9dd5009028a76f4b2cc9370dc56551d2355fad8 + languageName: node + linkType: hard + +"dunder-proto@npm:^1.0.1": + version: 1.0.1 + resolution: "dunder-proto@npm:1.0.1" + dependencies: + call-bind-apply-helpers: "npm:^1.0.1" + es-errors: "npm:^1.3.0" + gopd: "npm:^1.2.0" + checksum: 10c0/199f2a0c1c16593ca0a145dbf76a962f8033ce3129f01284d48c45ed4e14fea9bbacd7b3610b6cdc33486cef20385ac054948fefc6272fcce645c09468f93031 + languageName: node + linkType: hard + +"ecdsa-sig-formatter@npm:1.0.11": + version: 1.0.11 + resolution: "ecdsa-sig-formatter@npm:1.0.11" + dependencies: + safe-buffer: "npm:^5.0.1" + checksum: 10c0/ebfbf19d4b8be938f4dd4a83b8788385da353d63307ede301a9252f9f7f88672e76f2191618fd8edfc2f24679236064176fab0b78131b161ee73daa37125408c + languageName: node + linkType: hard + +"ee-first@npm:1.1.1": + version: 1.1.1 + resolution: "ee-first@npm:1.1.1" + checksum: 10c0/b5bb125ee93161bc16bfe6e56c6b04de5ad2aa44234d8f644813cc95d861a6910903132b05093706de2b706599367c4130eb6d170f6b46895686b95f87d017b7 + languageName: node + linkType: hard + +"encodeurl@npm:^2.0.0": + version: 2.0.0 + resolution: "encodeurl@npm:2.0.0" + checksum: 10c0/5d317306acb13e6590e28e27924c754163946a2480de11865c991a3a7eed4315cd3fba378b543ca145829569eefe9b899f3d84bb09870f675ae60bc924b01ceb + languageName: node + linkType: hard + +"encoding@npm:^0.1.13": + version: 0.1.13 + resolution: "encoding@npm:0.1.13" + dependencies: + iconv-lite: "npm:^0.6.2" + checksum: 10c0/36d938712ff00fe1f4bac88b43bcffb5930c1efa57bbcdca9d67e1d9d6c57cfb1200fb01efe0f3109b2ce99b231f90779532814a81370a1bd3274a0f58585039 + languageName: node + linkType: hard + +"env-paths@npm:^2.2.0": + version: 2.2.1 + resolution: "env-paths@npm:2.2.1" + checksum: 10c0/285325677bf00e30845e330eec32894f5105529db97496ee3f598478e50f008c5352a41a30e5e72ec9de8a542b5a570b85699cd63bd2bc646dbcb9f311d83bc4 + languageName: node + linkType: hard + +"err-code@npm:^2.0.2": + version: 2.0.3 + resolution: "err-code@npm:2.0.3" + checksum: 10c0/b642f7b4dd4a376e954947550a3065a9ece6733ab8e51ad80db727aaae0817c2e99b02a97a3d6cecc648a97848305e728289cf312d09af395403a90c9d4d8a66 + languageName: node + linkType: hard + +"es-define-property@npm:^1.0.1": + version: 1.0.1 + resolution: "es-define-property@npm:1.0.1" + checksum: 10c0/3f54eb49c16c18707949ff25a1456728c883e81259f045003499efba399c08bad00deebf65cccde8c0e07908c1a225c9d472b7107e558f2a48e28d530e34527c + languageName: node + linkType: hard + +"es-errors@npm:^1.3.0": + version: 1.3.0 + resolution: "es-errors@npm:1.3.0" + checksum: 10c0/0a61325670072f98d8ae3b914edab3559b6caa980f08054a3b872052640d91da01d38df55df797fcc916389d77fc92b8d5906cf028f4db46d7e3003abecbca85 + languageName: node + linkType: hard + +"es-module-lexer@npm:^2.0.0": + version: 2.0.0 + resolution: "es-module-lexer@npm:2.0.0" + checksum: 10c0/ae78dbbd43035a4b972c46cfb6877e374ea290adfc62bc2f5a083fea242c0b2baaab25c5886af86be55f092f4a326741cb94334cd3c478c383fdc8a9ec5ff817 + languageName: node + linkType: hard + +"es-object-atoms@npm:^1.0.0, es-object-atoms@npm:^1.1.1": + version: 1.1.1 + resolution: "es-object-atoms@npm:1.1.1" + dependencies: + es-errors: "npm:^1.3.0" + checksum: 10c0/65364812ca4daf48eb76e2a3b7a89b3f6a2e62a1c420766ce9f692665a29d94fe41fe88b65f24106f449859549711e4b40d9fb8002d862dfd7eb1c512d10be0c + languageName: node + linkType: hard + +"es-set-tostringtag@npm:^2.1.0": + version: 2.1.0 + resolution: "es-set-tostringtag@npm:2.1.0" + dependencies: + es-errors: "npm:^1.3.0" + get-intrinsic: "npm:^1.2.6" + has-tostringtag: "npm:^1.0.2" + hasown: "npm:^2.0.2" + checksum: 10c0/ef2ca9ce49afe3931cb32e35da4dcb6d86ab02592cfc2ce3e49ced199d9d0bb5085fc7e73e06312213765f5efa47cc1df553a6a5154584b21448e9fb8355b1af + languageName: node + linkType: hard + +"esbuild@npm:~0.27.0": + version: 0.27.3 + resolution: "esbuild@npm:0.27.3" + dependencies: + "@esbuild/aix-ppc64": "npm:0.27.3" + "@esbuild/android-arm": "npm:0.27.3" + "@esbuild/android-arm64": "npm:0.27.3" + "@esbuild/android-x64": "npm:0.27.3" + "@esbuild/darwin-arm64": "npm:0.27.3" + "@esbuild/darwin-x64": "npm:0.27.3" + "@esbuild/freebsd-arm64": "npm:0.27.3" + "@esbuild/freebsd-x64": "npm:0.27.3" + "@esbuild/linux-arm": "npm:0.27.3" + "@esbuild/linux-arm64": "npm:0.27.3" + "@esbuild/linux-ia32": "npm:0.27.3" + "@esbuild/linux-loong64": "npm:0.27.3" + "@esbuild/linux-mips64el": "npm:0.27.3" + "@esbuild/linux-ppc64": "npm:0.27.3" + "@esbuild/linux-riscv64": "npm:0.27.3" + "@esbuild/linux-s390x": "npm:0.27.3" + "@esbuild/linux-x64": "npm:0.27.3" + "@esbuild/netbsd-arm64": "npm:0.27.3" + "@esbuild/netbsd-x64": "npm:0.27.3" + "@esbuild/openbsd-arm64": "npm:0.27.3" + "@esbuild/openbsd-x64": "npm:0.27.3" + "@esbuild/openharmony-arm64": "npm:0.27.3" + "@esbuild/sunos-x64": "npm:0.27.3" + "@esbuild/win32-arm64": "npm:0.27.3" + "@esbuild/win32-ia32": "npm:0.27.3" + "@esbuild/win32-x64": "npm:0.27.3" + dependenciesMeta: + "@esbuild/aix-ppc64": + optional: true + "@esbuild/android-arm": + optional: true + "@esbuild/android-arm64": + optional: true + "@esbuild/android-x64": + optional: true + "@esbuild/darwin-arm64": + optional: true + "@esbuild/darwin-x64": + optional: true + "@esbuild/freebsd-arm64": + optional: true + "@esbuild/freebsd-x64": + optional: true + "@esbuild/linux-arm": + optional: true + "@esbuild/linux-arm64": + optional: true + "@esbuild/linux-ia32": + optional: true + "@esbuild/linux-loong64": + optional: true + "@esbuild/linux-mips64el": + optional: true + "@esbuild/linux-ppc64": + optional: true + "@esbuild/linux-riscv64": + optional: true + "@esbuild/linux-s390x": + optional: true + "@esbuild/linux-x64": + optional: true + "@esbuild/netbsd-arm64": + optional: true + "@esbuild/netbsd-x64": + optional: true + "@esbuild/openbsd-arm64": + optional: true + "@esbuild/openbsd-x64": + optional: true + "@esbuild/openharmony-arm64": + optional: true + "@esbuild/sunos-x64": + optional: true + "@esbuild/win32-arm64": + optional: true + "@esbuild/win32-ia32": + optional: true + "@esbuild/win32-x64": + optional: true + bin: + esbuild: bin/esbuild + checksum: 10c0/fdc3f87a3f08b3ef98362f37377136c389a0d180fda4b8d073b26ba930cf245521db0a368f119cc7624bc619248fff1439f5811f062d853576f8ffa3df8ee5f1 + languageName: node + linkType: hard + +"escape-html@npm:^1.0.3": + version: 1.0.3 + resolution: "escape-html@npm:1.0.3" + checksum: 10c0/524c739d776b36c3d29fa08a22e03e8824e3b2fd57500e5e44ecf3cc4707c34c60f9ca0781c0e33d191f2991161504c295e98f68c78fe7baa6e57081ec6ac0a3 + languageName: node + linkType: hard + +"escape-string-regexp@npm:^4.0.0": + version: 4.0.0 + resolution: "escape-string-regexp@npm:4.0.0" + checksum: 10c0/9497d4dd307d845bd7f75180d8188bb17ea8c151c1edbf6b6717c100e104d629dc2dfb687686181b0f4b7d732c7dfdc4d5e7a8ff72de1b0ca283a75bbb3a9cd9 + languageName: node + linkType: hard + +"eslint-config-prettier@npm:^10.1.8": + version: 10.1.8 + resolution: "eslint-config-prettier@npm:10.1.8" + peerDependencies: + eslint: ">=7.0.0" + bin: + eslint-config-prettier: bin/cli.js + checksum: 10c0/e1bcfadc9eccd526c240056b1e59c5cd26544fe59feb85f38f4f1f116caed96aea0b3b87868e68b3099e55caaac3f2e5b9f58110f85db893e83a332751192682 + languageName: node + linkType: hard + +"eslint-plugin-prettier@npm:^5.5.5": + version: 5.5.5 + resolution: "eslint-plugin-prettier@npm:5.5.5" + dependencies: + prettier-linter-helpers: "npm:^1.0.1" + synckit: "npm:^0.11.12" + peerDependencies: + "@types/eslint": ">=8.0.0" + eslint: ">=8.0.0" + eslint-config-prettier: ">= 7.0.0 <10.0.0 || >=10.1.0" + prettier: ">=3.0.0" + peerDependenciesMeta: + "@types/eslint": + optional: true + eslint-config-prettier: + optional: true + checksum: 10c0/091449b28c77ab2efbbf674e977181f2c8453d95a4df68218bddd87a4dfaa9ecc4eda60164e416f5986fb5d577e66e8d8e1e23d81e8555f8d735375598b03257 + languageName: node + linkType: hard + +"eslint-plugin-security@npm:^4.0.0": + version: 4.0.0 + resolution: "eslint-plugin-security@npm:4.0.0" + dependencies: + safe-regex: "npm:^2.1.1" + checksum: 10c0/559f315d7ad6131f1bc1cd5738dc82f6eecab7fec12134049a0420b50b59e2ab11e0cbc676ddeae84b111014102ecc7433dfe8b11d1f012821a7628610c8500d + languageName: node + linkType: hard + +"eslint-scope@npm:^9.1.2": + version: 9.1.2 + resolution: "eslint-scope@npm:9.1.2" + dependencies: + "@types/esrecurse": "npm:^4.3.1" + "@types/estree": "npm:^1.0.8" + esrecurse: "npm:^4.3.0" + estraverse: "npm:^5.2.0" + checksum: 10c0/9fb8bca5a73e5741efb6cec84467027b6cb6f4203ff9b43a938e272c5cd30800bde46a5c20dfd1609f840225f0b62b7673be391b20acadf8658ca9fa4729b3dd + languageName: node + linkType: hard + +"eslint-visitor-keys@npm:^3.4.3": + version: 3.4.3 + resolution: "eslint-visitor-keys@npm:3.4.3" + checksum: 10c0/92708e882c0a5ffd88c23c0b404ac1628cf20104a108c745f240a13c332a11aac54f49a22d5762efbffc18ecbc9a580d1b7ad034bf5f3cc3307e5cbff2ec9820 + languageName: node + linkType: hard + +"eslint-visitor-keys@npm:^5.0.1": + version: 5.0.1 + resolution: "eslint-visitor-keys@npm:5.0.1" + checksum: 10c0/16190bdf2cbae40a1109384c94450c526a79b0b9c3cb21e544256ed85ac48a4b84db66b74a6561d20fe6ab77447f150d711c2ad5ad74df4fcc133736bce99678 + languageName: node + linkType: hard + +"eslint@npm:^10.0.3": + version: 10.0.3 + resolution: "eslint@npm:10.0.3" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.8.0" + "@eslint-community/regexpp": "npm:^4.12.2" + "@eslint/config-array": "npm:^0.23.3" + "@eslint/config-helpers": "npm:^0.5.2" + "@eslint/core": "npm:^1.1.1" + "@eslint/plugin-kit": "npm:^0.6.1" + "@humanfs/node": "npm:^0.16.6" + "@humanwhocodes/module-importer": "npm:^1.0.1" + "@humanwhocodes/retry": "npm:^0.4.2" + "@types/estree": "npm:^1.0.6" + ajv: "npm:^6.14.0" + cross-spawn: "npm:^7.0.6" + debug: "npm:^4.3.2" + escape-string-regexp: "npm:^4.0.0" + eslint-scope: "npm:^9.1.2" + eslint-visitor-keys: "npm:^5.0.1" + espree: "npm:^11.1.1" + esquery: "npm:^1.7.0" + esutils: "npm:^2.0.2" + fast-deep-equal: "npm:^3.1.3" + file-entry-cache: "npm:^8.0.0" + find-up: "npm:^5.0.0" + glob-parent: "npm:^6.0.2" + ignore: "npm:^5.2.0" + imurmurhash: "npm:^0.1.4" + is-glob: "npm:^4.0.0" + json-stable-stringify-without-jsonify: "npm:^1.0.1" + minimatch: "npm:^10.2.4" + natural-compare: "npm:^1.4.0" + optionator: "npm:^0.9.3" + peerDependencies: + jiti: "*" + peerDependenciesMeta: + jiti: + optional: true + bin: + eslint: bin/eslint.js + checksum: 10c0/fbbb4d99cb6af5c30b163b7898241dbac1cd1cee0e6746d5732a95e3b1e68b5bea0bc27cb78e8440a39cf4cc98c7f52cf5ed8d7c2bbdf2232662476d113c41fc + languageName: node + linkType: hard + +"espree@npm:^11.1.1": + version: 11.1.1 + resolution: "espree@npm:11.1.1" + dependencies: + acorn: "npm:^8.16.0" + acorn-jsx: "npm:^5.3.2" + eslint-visitor-keys: "npm:^5.0.1" + checksum: 10c0/2feae74efdfb037b9e9fcb30506799845cf20900de5e441ed03e5c51aaa249f85ea5818ff177682acc0c9bfb4ac97e1965c238ee44ac7c305aab8747177bab69 + languageName: node + linkType: hard + +"esquery@npm:^1.7.0": + version: 1.7.0 + resolution: "esquery@npm:1.7.0" + dependencies: + estraverse: "npm:^5.1.0" + checksum: 10c0/77d5173db450b66f3bc685d11af4c90cffeedb340f34a39af96d43509a335ce39c894fd79233df32d38f5e4e219fa0f7076f6ec90bae8320170ba082c0db4793 + languageName: node + linkType: hard + +"esrecurse@npm:^4.3.0": + version: 4.3.0 + resolution: "esrecurse@npm:4.3.0" + dependencies: + estraverse: "npm:^5.2.0" + checksum: 10c0/81a37116d1408ded88ada45b9fb16dbd26fba3aadc369ce50fcaf82a0bac12772ebd7b24cd7b91fc66786bf2c1ac7b5f196bc990a473efff972f5cb338877cf5 + languageName: node + linkType: hard + +"estraverse@npm:^5.1.0, estraverse@npm:^5.2.0": + version: 5.3.0 + resolution: "estraverse@npm:5.3.0" + checksum: 10c0/1ff9447b96263dec95d6d67431c5e0771eb9776427421260a3e2f0fdd5d6bd4f8e37a7338f5ad2880c9f143450c9b1e4fc2069060724570a49cf9cf0312bd107 + languageName: node + linkType: hard + +"estree-walker@npm:^3.0.3": + version: 3.0.3 + resolution: "estree-walker@npm:3.0.3" + dependencies: + "@types/estree": "npm:^1.0.0" + checksum: 10c0/c12e3c2b2642d2bcae7d5aa495c60fa2f299160946535763969a1c83fc74518ffa9c2cd3a8b69ac56aea547df6a8aac25f729a342992ef0bbac5f1c73e78995d + languageName: node + linkType: hard + +"esutils@npm:^2.0.2": + version: 2.0.3 + resolution: "esutils@npm:2.0.3" + checksum: 10c0/9a2fe69a41bfdade834ba7c42de4723c97ec776e40656919c62cbd13607c45e127a003f05f724a1ea55e5029a4cf2de444b13009f2af71271e42d93a637137c7 + languageName: node + linkType: hard + +"etag@npm:^1.8.1": + version: 1.8.1 + resolution: "etag@npm:1.8.1" + checksum: 10c0/12be11ef62fb9817314d790089a0a49fae4e1b50594135dcb8076312b7d7e470884b5100d249b28c18581b7fd52f8b485689ffae22a11ed9ec17377a33a08f84 + languageName: node + linkType: hard + +"expect-type@npm:^1.3.0": + version: 1.3.0 + resolution: "expect-type@npm:1.3.0" + checksum: 10c0/8412b3fe4f392c420ab41dae220b09700e4e47c639a29ba7ba2e83cc6cffd2b4926f7ac9e47d7e277e8f4f02acda76fd6931cb81fd2b382fa9477ef9ada953fd + languageName: node + linkType: hard + +"exponential-backoff@npm:^3.1.1": + version: 3.1.3 + resolution: "exponential-backoff@npm:3.1.3" + checksum: 10c0/77e3ae682b7b1f4972f563c6dbcd2b0d54ac679e62d5d32f3e5085feba20483cf28bd505543f520e287a56d4d55a28d7874299941faf637e779a1aa5994d1267 + languageName: node + linkType: hard + +"express-rate-limit@npm:8": + version: 8.2.1 + resolution: "express-rate-limit@npm:8.2.1" + dependencies: + ip-address: "npm:10.0.1" + peerDependencies: + express: ">= 4.11" + checksum: 10c0/54185f211c25655382436b8ad1a2136df0d5dc88f4d9d4438ca7cbc87cef0cd34cb01b8fc62d290445326aa6581470d2ff44502c3f1a34a5ed2c2ce56809fa01 + languageName: node + linkType: hard + +"express-rate-limit@npm:^8.3.1": + version: 8.3.1 + resolution: "express-rate-limit@npm:8.3.1" + dependencies: + ip-address: "npm:10.1.0" + peerDependencies: + express: ">= 4.11" + checksum: 10c0/e3229938457cec617460c54ef4e90c8c254facc884729325d20ea35e3838bd273e4c611fc1f4a76f6d14c411e30d31b15e88eb4be87408615ff0aacc142511a2 + languageName: node + linkType: hard + +"express-session@npm:^1.19.0": + version: 1.19.0 + resolution: "express-session@npm:1.19.0" + dependencies: + cookie: "npm:~0.7.2" + cookie-signature: "npm:~1.0.7" + debug: "npm:~2.6.9" + depd: "npm:~2.0.0" + on-headers: "npm:~1.1.0" + parseurl: "npm:~1.3.3" + safe-buffer: "npm:~5.2.1" + uid-safe: "npm:~2.1.5" + checksum: 10c0/b1766010a728c58ca1b93ea33d49008b0b16a8751e5767d36b5bf3dd2f7f77d9933bb55352d296edbad4824c8c65e33554f058a33f201406557c1b3615fd10dc + languageName: node + linkType: hard + +"express-slow-down@npm:^3.1.0": + version: 3.1.0 + resolution: "express-slow-down@npm:3.1.0" + dependencies: + express-rate-limit: "npm:8" + peerDependencies: + express: 4 || 5 || ^5.0.0-beta.1 + checksum: 10c0/58c4494ed8d84fa52c3bc9b820e7f2a06059e2c70afa3323f1f009031f8438c978eed0e7b8d4eeb066489b6e8a8635dd812ecdf66a4b6b67971ce418ed4d6256 + languageName: node + linkType: hard + +"express-twtkpr@workspace:.": + version: 0.0.0-use.local + resolution: "express-twtkpr@workspace:." + dependencies: + "@cacheable/node-cache": "npm:^2.0.2" + "@exodus/blakejs": "npm:^1.1.1-exodus.0" + "@types/cookie-parser": "npm:^1.4.10" + "@types/cors": "npm:^2.8.19" + "@types/debug": "npm:^4.1.12" + "@types/express": "npm:^5.0.6" + "@types/express-session": "npm:^1.18.2" + "@types/formidable": "npm:^3.5.0" + "@types/jsonwebtoken": "npm:^9.0.10" + "@types/morgan": "npm:^1.9.10" + "@types/node": "npm:^25.5.0" + "@types/supertest": "npm:^7.2.0" + base32.js: "npm:^0.1.0" + bcryptjs: "npm:^3.0.3" + cookie-parser: "npm:^1.4.7" + dayjs: "npm:^1.11.20" + debug: "npm:^4.4.3" + eslint: "npm:^10.0.3" + eslint-config-prettier: "npm:^10.1.8" + eslint-plugin-prettier: "npm:^5.5.5" + eslint-plugin-security: "npm:^4.0.0" + express: "npm:^5.2.1" + express-rate-limit: "npm:^8.3.1" + express-session: "npm:^1.19.0" + express-slow-down: "npm:^3.1.0" + formidable: "npm:^3.5.4" + jsonwebtoken: "npm:^9.0.3" + link: "npm:^2.1.2" + prettier: "npm:^3.8.1" + session-file-store: "npm:^1.5.0" + supertest: "npm:^7.2.2" + tsx: "npm:^4.21.0" + twtxt-lib: "npm:^0.9.4" + typescript: "npm:^5.9.3" + uuid: "npm:^13.0.0" + vitest: "npm:^4.1.0" + zod: "npm:^4.3.6" + languageName: unknown + linkType: soft + +"express@npm:^5.2.1": + version: 5.2.1 + resolution: "express@npm:5.2.1" + dependencies: + accepts: "npm:^2.0.0" + body-parser: "npm:^2.2.1" + content-disposition: "npm:^1.0.0" + content-type: "npm:^1.0.5" + cookie: "npm:^0.7.1" + cookie-signature: "npm:^1.2.1" + debug: "npm:^4.4.0" + depd: "npm:^2.0.0" + encodeurl: "npm:^2.0.0" + escape-html: "npm:^1.0.3" + etag: "npm:^1.8.1" + finalhandler: "npm:^2.1.0" + fresh: "npm:^2.0.0" + http-errors: "npm:^2.0.0" + merge-descriptors: "npm:^2.0.0" + mime-types: "npm:^3.0.0" + on-finished: "npm:^2.4.1" + once: "npm:^1.4.0" + parseurl: "npm:^1.3.3" + proxy-addr: "npm:^2.0.7" + qs: "npm:^6.14.0" + range-parser: "npm:^1.2.1" + router: "npm:^2.2.0" + send: "npm:^1.1.0" + serve-static: "npm:^2.2.0" + statuses: "npm:^2.0.1" + type-is: "npm:^2.0.1" + vary: "npm:^1.1.2" + checksum: 10c0/45e8c841ad188a41402ddcd1294901e861ee0819f632fb494f2ed344ef9c43315d294d443fb48d594e6586a3b779785120f43321417adaef8567316a55072949 + languageName: node + linkType: hard + +"fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": + version: 3.1.3 + resolution: "fast-deep-equal@npm:3.1.3" + checksum: 10c0/40dedc862eb8992c54579c66d914635afbec43350afbbe991235fdcb4e3a8d5af1b23ae7e79bef7d4882d0ecee06c3197488026998fb19f72dc95acff1d1b1d0 + languageName: node + linkType: hard + +"fast-diff@npm:^1.1.2": + version: 1.3.0 + resolution: "fast-diff@npm:1.3.0" + checksum: 10c0/5c19af237edb5d5effda008c891a18a585f74bf12953be57923f17a3a4d0979565fc64dbc73b9e20926b9d895f5b690c618cbb969af0cf022e3222471220ad29 + languageName: node + linkType: hard + +"fast-json-stable-stringify@npm:^2.0.0": + version: 2.1.0 + resolution: "fast-json-stable-stringify@npm:2.1.0" + checksum: 10c0/7f081eb0b8a64e0057b3bb03f974b3ef00135fbf36c1c710895cd9300f13c94ba809bb3a81cf4e1b03f6e5285610a61abbd7602d0652de423144dfee5a389c9b + languageName: node + linkType: hard + +"fast-levenshtein@npm:^2.0.6": + version: 2.0.6 + resolution: "fast-levenshtein@npm:2.0.6" + checksum: 10c0/111972b37338bcb88f7d9e2c5907862c280ebf4234433b95bc611e518d192ccb2d38119c4ac86e26b668d75f7f3894f4ff5c4982899afced7ca78633b08287c4 + languageName: node + linkType: hard + +"fast-safe-stringify@npm:^2.1.1": + version: 2.1.1 + resolution: "fast-safe-stringify@npm:2.1.1" + checksum: 10c0/d90ec1c963394919828872f21edaa3ad6f1dddd288d2bd4e977027afff09f5db40f94e39536d4646f7e01761d704d72d51dce5af1b93717f3489ef808f5f4e4d + languageName: node + linkType: hard + +"fdir@npm:^6.5.0": + version: 6.5.0 + resolution: "fdir@npm:6.5.0" + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + checksum: 10c0/e345083c4306b3aed6cb8ec551e26c36bab5c511e99ea4576a16750ddc8d3240e63826cc624f5ae17ad4dc82e68a253213b60d556c11bfad064b7607847ed07f + languageName: node + linkType: hard + +"file-entry-cache@npm:^8.0.0": + version: 8.0.0 + resolution: "file-entry-cache@npm:8.0.0" + dependencies: + flat-cache: "npm:^4.0.0" + checksum: 10c0/9e2b5938b1cd9b6d7e3612bdc533afd4ac17b2fc646569e9a8abbf2eb48e5eb8e316bc38815a3ef6a1b456f4107f0d0f055a614ca613e75db6bf9ff4d72c1638 + languageName: node + linkType: hard + +"finalhandler@npm:^2.1.0": + version: 2.1.1 + resolution: "finalhandler@npm:2.1.1" + dependencies: + debug: "npm:^4.4.0" + encodeurl: "npm:^2.0.0" + escape-html: "npm:^1.0.3" + on-finished: "npm:^2.4.1" + parseurl: "npm:^1.3.3" + statuses: "npm:^2.0.1" + checksum: 10c0/6bd664e21b7b2e79efcaace7d1a427169f61cce048fae68eb56290e6934e676b78e55d89f5998c5508871345bc59a61f47002dc505dc7288be68cceac1b701e2 + languageName: node + linkType: hard + +"find-up@npm:^5.0.0": + version: 5.0.0 + resolution: "find-up@npm:5.0.0" + dependencies: + locate-path: "npm:^6.0.0" + path-exists: "npm:^4.0.0" + checksum: 10c0/062c5a83a9c02f53cdd6d175a37ecf8f87ea5bbff1fdfb828f04bfa021441bc7583e8ebc0872a4c1baab96221fb8a8a275a19809fb93fbc40bd69ec35634069a + languageName: node + linkType: hard + +"flat-cache@npm:^4.0.0": + version: 4.0.1 + resolution: "flat-cache@npm:4.0.1" + dependencies: + flatted: "npm:^3.2.9" + keyv: "npm:^4.5.4" + checksum: 10c0/2c59d93e9faa2523e4fda6b4ada749bed432cfa28c8e251f33b25795e426a1c6dbada777afb1f74fcfff33934fdbdea921ee738fcc33e71adc9d6eca984a1cfc + languageName: node + linkType: hard + +"flatted@npm:^3.2.9": + version: 3.3.3 + resolution: "flatted@npm:3.3.3" + checksum: 10c0/e957a1c6b0254aa15b8cce8533e24165abd98fadc98575db082b786b5da1b7d72062b81bfdcd1da2f4d46b6ed93bec2434e62333e9b4261d79ef2e75a10dd538 + languageName: node + linkType: hard + +"form-data@npm:^4.0.0, form-data@npm:^4.0.5": + version: 4.0.5 + resolution: "form-data@npm:4.0.5" + dependencies: + asynckit: "npm:^0.4.0" + combined-stream: "npm:^1.0.8" + es-set-tostringtag: "npm:^2.1.0" + hasown: "npm:^2.0.2" + mime-types: "npm:^2.1.12" + checksum: 10c0/dd6b767ee0bbd6d84039db12a0fa5a2028160ffbfaba1800695713b46ae974a5f6e08b3356c3195137f8530dcd9dfcb5d5ae1eeff53d0db1e5aad863b619ce3b + languageName: node + linkType: hard + +"formidable@npm:^3.5.4": + version: 3.5.4 + resolution: "formidable@npm:3.5.4" + dependencies: + "@paralleldrive/cuid2": "npm:^2.2.2" + dezalgo: "npm:^1.0.4" + once: "npm:^1.4.0" + checksum: 10c0/3a311ce57617eb8f532368e91c0f2bbfb299a0f1a35090e085bd6ca772298f196fbb0b66f0d4b5549d7bf3c5e1844439338d4402b7b6d1fedbe206ad44a931f8 + languageName: node + linkType: hard + +"forwarded@npm:0.2.0": + version: 0.2.0 + resolution: "forwarded@npm:0.2.0" + checksum: 10c0/9b67c3fac86acdbc9ae47ba1ddd5f2f81526fa4c8226863ede5600a3f7c7416ef451f6f1e240a3cc32d0fd79fcfe6beb08fd0da454f360032bde70bf80afbb33 + languageName: node + linkType: hard + +"fresh@npm:^2.0.0": + version: 2.0.0 + resolution: "fresh@npm:2.0.0" + checksum: 10c0/0557548194cb9a809a435bf92bcfbc20c89e8b5eb38861b73ced36750437251e39a111fc3a18b98531be9dd91fe1411e4969f229dc579ec0251ce6c5d4900bbc + languageName: node + linkType: hard + +"fs-extra@npm:^8.0.1": + version: 8.1.0 + resolution: "fs-extra@npm:8.1.0" + dependencies: + graceful-fs: "npm:^4.2.0" + jsonfile: "npm:^4.0.0" + universalify: "npm:^0.1.0" + checksum: 10c0/259f7b814d9e50d686899550c4f9ded85c46c643f7fe19be69504888e007fcbc08f306fae8ec495b8b998635e997c9e3e175ff2eeed230524ef1c1684cc96423 + languageName: node + linkType: hard + +"fs-minipass@npm:^3.0.0": + version: 3.0.3 + resolution: "fs-minipass@npm:3.0.3" + dependencies: + minipass: "npm:^7.0.3" + checksum: 10c0/63e80da2ff9b621e2cb1596abcb9207f1cf82b968b116ccd7b959e3323144cce7fb141462200971c38bbf2ecca51695069db45265705bed09a7cd93ae5b89f94 + languageName: node + linkType: hard + +"fsevents@npm:~2.3.3": + version: 2.3.3 + resolution: "fsevents@npm:2.3.3" + dependencies: + node-gyp: "npm:latest" + checksum: 10c0/a1f0c44595123ed717febbc478aa952e47adfc28e2092be66b8ab1635147254ca6cfe1df792a8997f22716d4cbafc73309899ff7bfac2ac3ad8cf2e4ecc3ec60 + conditions: os=darwin + languageName: node + linkType: hard + +"fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin": + version: 2.3.3 + resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" + dependencies: + node-gyp: "npm:latest" + conditions: os=darwin + languageName: node + linkType: hard + +"function-bind@npm:^1.1.2": + version: 1.1.2 + resolution: "function-bind@npm:1.1.2" + checksum: 10c0/d8680ee1e5fcd4c197e4ac33b2b4dce03c71f4d91717292785703db200f5c21f977c568d28061226f9b5900cbcd2c84463646134fd5337e7925e0942bc3f46d5 + languageName: node + linkType: hard + +"generator-function@npm:^2.0.0": + version: 2.0.1 + resolution: "generator-function@npm:2.0.1" + checksum: 10c0/8a9f59df0f01cfefafdb3b451b80555e5cf6d76487095db91ac461a0e682e4ff7a9dbce15f4ecec191e53586d59eece01949e05a4b4492879600bbbe8e28d6b8 + languageName: node + linkType: hard + +"get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.3.0": + version: 1.3.1 + resolution: "get-intrinsic@npm:1.3.1" + dependencies: + async-function: "npm:^1.0.0" + async-generator-function: "npm:^1.0.0" + call-bind-apply-helpers: "npm:^1.0.2" + es-define-property: "npm:^1.0.1" + es-errors: "npm:^1.3.0" + es-object-atoms: "npm:^1.1.1" + function-bind: "npm:^1.1.2" + generator-function: "npm:^2.0.0" + get-proto: "npm:^1.0.1" + gopd: "npm:^1.2.0" + has-symbols: "npm:^1.1.0" + hasown: "npm:^2.0.2" + math-intrinsics: "npm:^1.1.0" + checksum: 10c0/9f4ab0cf7efe0fd2c8185f52e6f637e708f3a112610c88869f8f041bb9ecc2ce44bf285dfdbdc6f4f7c277a5b88d8e94a432374d97cca22f3de7fc63795deb5d + languageName: node + linkType: hard + +"get-proto@npm:^1.0.1": + version: 1.0.1 + resolution: "get-proto@npm:1.0.1" + dependencies: + dunder-proto: "npm:^1.0.1" + es-object-atoms: "npm:^1.0.0" + checksum: 10c0/9224acb44603c5526955e83510b9da41baf6ae73f7398875fba50edc5e944223a89c4a72b070fcd78beb5f7bdda58ecb6294adc28f7acfc0da05f76a2399643c + languageName: node + linkType: hard + +"get-tsconfig@npm:^4.7.5": + version: 4.13.6 + resolution: "get-tsconfig@npm:4.13.6" + dependencies: + resolve-pkg-maps: "npm:^1.0.0" + checksum: 10c0/bab6937302f542f97217cbe7cbbdfa7e85a56a377bc7a73e69224c1f0b7c9ae8365918e55752ae8648265903f506c1705f63c0de1d4bab1ec2830fef3e539a1a + languageName: node + linkType: hard + +"glob-parent@npm:^6.0.2": + version: 6.0.2 + resolution: "glob-parent@npm:6.0.2" + dependencies: + is-glob: "npm:^4.0.3" + checksum: 10c0/317034d88654730230b3f43bb7ad4f7c90257a426e872ea0bf157473ac61c99bf5d205fad8f0185f989be8d2fa6d3c7dce1645d99d545b6ea9089c39f838e7f8 + languageName: node + linkType: hard + +"glob@npm:^13.0.0": + version: 13.0.6 + resolution: "glob@npm:13.0.6" + dependencies: + minimatch: "npm:^10.2.2" + minipass: "npm:^7.1.3" + path-scurry: "npm:^2.0.2" + checksum: 10c0/269c236f11a9b50357fe7a8c6aadac667e01deb5242b19c84975628f05f4438d8ee1354bb62c5d6c10f37fd59911b54d7799730633a2786660d8c69f1d18120a + languageName: node + linkType: hard + +"gopd@npm:^1.2.0": + version: 1.2.0 + resolution: "gopd@npm:1.2.0" + checksum: 10c0/50fff1e04ba2b7737c097358534eacadad1e68d24cccee3272e04e007bed008e68d2614f3987788428fd192a5ae3889d08fb2331417e4fc4a9ab366b2043cead + languageName: node + linkType: hard + +"graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.6": + version: 4.2.11 + resolution: "graceful-fs@npm:4.2.11" + checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2 + languageName: node + linkType: hard + +"has-symbols@npm:^1.0.3, has-symbols@npm:^1.1.0": + version: 1.1.0 + resolution: "has-symbols@npm:1.1.0" + checksum: 10c0/dde0a734b17ae51e84b10986e651c664379018d10b91b6b0e9b293eddb32f0f069688c841fb40f19e9611546130153e0a2a48fd7f512891fb000ddfa36f5a20e + languageName: node + linkType: hard + +"has-tostringtag@npm:^1.0.2": + version: 1.0.2 + resolution: "has-tostringtag@npm:1.0.2" + dependencies: + has-symbols: "npm:^1.0.3" + checksum: 10c0/a8b166462192bafe3d9b6e420a1d581d93dd867adb61be223a17a8d6dad147aa77a8be32c961bb2f27b3ef893cae8d36f564ab651f5e9b7938ae86f74027c48c + languageName: node + linkType: hard + +"hashery@npm:^1.5.0": + version: 1.5.0 + resolution: "hashery@npm:1.5.0" + dependencies: + hookified: "npm:^1.14.0" + checksum: 10c0/475ebc8edc238aa91a2a63400d4ad7b51f823f1920100053d8f6fa3f0d44bff47911185269069e22a3fdcbf68db405ad5bdc197c784f75f8923e689526843d3c + languageName: node + linkType: hard + +"hasown@npm:^2.0.2": + version: 2.0.2 + resolution: "hasown@npm:2.0.2" + dependencies: + function-bind: "npm:^1.1.2" + checksum: 10c0/3769d434703b8ac66b209a4cca0737519925bbdb61dd887f93a16372b14694c63ff4e797686d87c90f08168e81082248b9b028bad60d4da9e0d1148766f56eb9 + languageName: node + linkType: hard + +"hookified@npm:^1.14.0, hookified@npm:^1.15.0": + version: 1.15.1 + resolution: "hookified@npm:1.15.1" + checksum: 10c0/6b691374fa97ae57169fb29f90e723499fda5e85494654fbe55c4768b3ccbf3e14c0adc8d0f365f32c503b60d7c06f907781f5966c03d41c423575eb5e16860c + languageName: node + linkType: hard + +"http-cache-semantics@npm:^4.1.1": + version: 4.2.0 + resolution: "http-cache-semantics@npm:4.2.0" + checksum: 10c0/45b66a945cf13ec2d1f29432277201313babf4a01d9e52f44b31ca923434083afeca03f18417f599c9ab3d0e7b618ceb21257542338b57c54b710463b4a53e37 + languageName: node + linkType: hard + +"http-errors@npm:^2.0.0, http-errors@npm:^2.0.1, http-errors@npm:~2.0.1": + version: 2.0.1 + resolution: "http-errors@npm:2.0.1" + dependencies: + depd: "npm:~2.0.0" + inherits: "npm:~2.0.4" + setprototypeof: "npm:~1.2.0" + statuses: "npm:~2.0.2" + toidentifier: "npm:~1.0.1" + checksum: 10c0/fb38906cef4f5c83952d97661fe14dc156cb59fe54812a42cd448fa57b5c5dfcb38a40a916957737bd6b87aab257c0648d63eb5b6a9ca9f548e105b6072712d4 + languageName: node + linkType: hard + +"http-proxy-agent@npm:^7.0.0": + version: 7.0.2 + resolution: "http-proxy-agent@npm:7.0.2" + dependencies: + agent-base: "npm:^7.1.0" + debug: "npm:^4.3.4" + checksum: 10c0/4207b06a4580fb85dd6dff521f0abf6db517489e70863dca1a0291daa7f2d3d2d6015a57bd702af068ea5cf9f1f6ff72314f5f5b4228d299c0904135d2aef921 + languageName: node + linkType: hard + +"https-proxy-agent@npm:^7.0.1": + version: 7.0.6 + resolution: "https-proxy-agent@npm:7.0.6" + dependencies: + agent-base: "npm:^7.1.2" + debug: "npm:4" + checksum: 10c0/f729219bc735edb621fa30e6e84e60ee5d00802b8247aac0d7b79b0bd6d4b3294737a337b93b86a0bd9e68099d031858a39260c976dc14cdbba238ba1f8779ac + languageName: node + linkType: hard + +"iconv-lite@npm:^0.6.2": + version: 0.6.3 + resolution: "iconv-lite@npm:0.6.3" + dependencies: + safer-buffer: "npm:>= 2.1.2 < 3.0.0" + checksum: 10c0/98102bc66b33fcf5ac044099d1257ba0b7ad5e3ccd3221f34dd508ab4070edff183276221684e1e0555b145fce0850c9f7d2b60a9fcac50fbb4ea0d6e845a3b1 + languageName: node + linkType: hard + +"iconv-lite@npm:^0.7.0, iconv-lite@npm:~0.7.0": + version: 0.7.2 + resolution: "iconv-lite@npm:0.7.2" + dependencies: + safer-buffer: "npm:>= 2.1.2 < 3.0.0" + checksum: 10c0/3c228920f3bd307f56bf8363706a776f4a060eb042f131cd23855ceca962951b264d0997ab38a1ad340e1c5df8499ed26e1f4f0db6b2a2ad9befaff22f14b722 + languageName: node + linkType: hard + +"ignore@npm:^5.2.0": + version: 5.3.2 + resolution: "ignore@npm:5.3.2" + checksum: 10c0/f9f652c957983634ded1e7f02da3b559a0d4cc210fca3792cb67f1b153623c9c42efdc1c4121af171e295444459fc4a9201101fb041b1104a3c000bccb188337 + languageName: node + linkType: hard + +"imurmurhash@npm:^0.1.4": + version: 0.1.4 + resolution: "imurmurhash@npm:0.1.4" + checksum: 10c0/8b51313850dd33605c6c9d3fd9638b714f4c4c40250cff658209f30d40da60f78992fb2df5dabee4acf589a6a82bbc79ad5486550754bd9ec4e3fc0d4a57d6a6 + languageName: node + linkType: hard + +"inherits@npm:^2.0.1, inherits@npm:~2.0.4": + version: 2.0.4 + resolution: "inherits@npm:2.0.4" + checksum: 10c0/4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2 + languageName: node + linkType: hard + +"ip-address@npm:10.0.1": + version: 10.0.1 + resolution: "ip-address@npm:10.0.1" + checksum: 10c0/1634d79dae18394004775cb6d699dc46b7c23df6d2083164025a2b15240c1164fccde53d0e08bd5ee4fc53913d033ab6b5e395a809ad4b956a940c446e948843 + languageName: node + linkType: hard + +"ip-address@npm:10.1.0, ip-address@npm:^10.0.1": + version: 10.1.0 + resolution: "ip-address@npm:10.1.0" + checksum: 10c0/0103516cfa93f6433b3bd7333fa876eb21263912329bfa47010af5e16934eeeff86f3d2ae700a3744a137839ddfad62b900c7a445607884a49b5d1e32a3d7566 + languageName: node + linkType: hard + +"ipaddr.js@npm:1.9.1": + version: 1.9.1 + resolution: "ipaddr.js@npm:1.9.1" + checksum: 10c0/0486e775047971d3fdb5fb4f063829bac45af299ae0b82dcf3afa2145338e08290563a2a70f34b732d795ecc8311902e541a8530eeb30d75860a78ff4e94ce2a + languageName: node + linkType: hard + +"is-extglob@npm:^2.1.1": + version: 2.1.1 + resolution: "is-extglob@npm:2.1.1" + checksum: 10c0/5487da35691fbc339700bbb2730430b07777a3c21b9ebaecb3072512dfd7b4ba78ac2381a87e8d78d20ea08affb3f1971b4af629173a6bf435ff8a4c47747912 + languageName: node + linkType: hard + +"is-glob@npm:^4.0.0, is-glob@npm:^4.0.3": + version: 4.0.3 + resolution: "is-glob@npm:4.0.3" + dependencies: + is-extglob: "npm:^2.1.1" + checksum: 10c0/17fb4014e22be3bbecea9b2e3a76e9e34ff645466be702f1693e8f1ee1adac84710d0be0bd9f967d6354036fd51ab7c2741d954d6e91dae6bb69714de92c197a + languageName: node + linkType: hard + +"is-promise@npm:^4.0.0": + version: 4.0.0 + resolution: "is-promise@npm:4.0.0" + checksum: 10c0/ebd5c672d73db781ab33ccb155fb9969d6028e37414d609b115cc534654c91ccd061821d5b987eefaa97cf4c62f0b909bb2f04db88306de26e91bfe8ddc01503 + languageName: node + linkType: hard + +"is-typedarray@npm:^1.0.0": + version: 1.0.0 + resolution: "is-typedarray@npm:1.0.0" + checksum: 10c0/4c096275ba041a17a13cca33ac21c16bc4fd2d7d7eb94525e7cd2c2f2c1a3ab956e37622290642501ff4310601e413b675cf399ad6db49855527d2163b3eeeec + languageName: node + linkType: hard + +"isexe@npm:^2.0.0": + version: 2.0.0 + resolution: "isexe@npm:2.0.0" + checksum: 10c0/228cfa503fadc2c31596ab06ed6aa82c9976eec2bfd83397e7eaf06d0ccf42cd1dfd6743bf9aeb01aebd4156d009994c5f76ea898d2832c1fe342da923ca457d + languageName: node + linkType: hard + +"isexe@npm:^4.0.0": + version: 4.0.0 + resolution: "isexe@npm:4.0.0" + checksum: 10c0/5884815115bceac452877659a9c7726382531592f43dc29e5d48b7c4100661aed54018cb90bd36cb2eaeba521092570769167acbb95c18d39afdccbcca06c5ce + languageName: node + linkType: hard + +"json-buffer@npm:3.0.1": + version: 3.0.1 + resolution: "json-buffer@npm:3.0.1" + checksum: 10c0/0d1c91569d9588e7eef2b49b59851f297f3ab93c7b35c7c221e288099322be6b562767d11e4821da500f3219542b9afd2e54c5dc573107c1126ed1080f8e96d7 + languageName: node + linkType: hard + +"json-schema-traverse@npm:^0.4.1": + version: 0.4.1 + resolution: "json-schema-traverse@npm:0.4.1" + checksum: 10c0/108fa90d4cc6f08243aedc6da16c408daf81793bf903e9fd5ab21983cda433d5d2da49e40711da016289465ec2e62e0324dcdfbc06275a607fe3233fde4942ce + languageName: node + linkType: hard + +"json-stable-stringify-without-jsonify@npm:^1.0.1": + version: 1.0.1 + resolution: "json-stable-stringify-without-jsonify@npm:1.0.1" + checksum: 10c0/cb168b61fd4de83e58d09aaa6425ef71001bae30d260e2c57e7d09a5fd82223e2f22a042dedaab8db23b7d9ae46854b08bb1f91675a8be11c5cffebef5fb66a5 + languageName: node + linkType: hard + +"jsonfile@npm:^4.0.0": + version: 4.0.0 + resolution: "jsonfile@npm:4.0.0" + dependencies: + graceful-fs: "npm:^4.1.6" + dependenciesMeta: + graceful-fs: + optional: true + checksum: 10c0/7dc94b628d57a66b71fb1b79510d460d662eb975b5f876d723f81549c2e9cd316d58a2ddf742b2b93a4fa6b17b2accaf1a738a0e2ea114bdfb13a32e5377e480 + languageName: node + linkType: hard + +"jsonwebtoken@npm:^9.0.3": + version: 9.0.3 + resolution: "jsonwebtoken@npm:9.0.3" + dependencies: + jws: "npm:^4.0.1" + lodash.includes: "npm:^4.3.0" + lodash.isboolean: "npm:^3.0.3" + lodash.isinteger: "npm:^4.0.4" + lodash.isnumber: "npm:^3.0.3" + lodash.isplainobject: "npm:^4.0.6" + lodash.isstring: "npm:^4.0.1" + lodash.once: "npm:^4.0.0" + ms: "npm:^2.1.1" + semver: "npm:^7.5.4" + checksum: 10c0/6ca7f1e54886ea3bde7146a5a22b53847c46e25453c7f7307a69818b9a6ad48c390b2e59d5690fcfd03c529b01960060cc4bb0c686991d6edae2285dfd30f4ba + languageName: node + linkType: hard + +"jwa@npm:^2.0.1": + version: 2.0.1 + resolution: "jwa@npm:2.0.1" + dependencies: + buffer-equal-constant-time: "npm:^1.0.1" + ecdsa-sig-formatter: "npm:1.0.11" + safe-buffer: "npm:^5.0.1" + checksum: 10c0/ab3ebc6598e10dc11419d4ed675c9ca714a387481466b10e8a6f3f65d8d9c9237e2826f2505280a739cf4cbcf511cb288eeec22b5c9c63286fc5a2e4f97e78cf + languageName: node + linkType: hard + +"jws@npm:^4.0.1": + version: 4.0.1 + resolution: "jws@npm:4.0.1" + dependencies: + jwa: "npm:^2.0.1" + safe-buffer: "npm:^5.0.1" + checksum: 10c0/6be1ed93023aef570ccc5ea8d162b065840f3ef12f0d1bb3114cade844de7a357d5dc558201d9a65101e70885a6fa56b17462f520e6b0d426195510618a154d0 + languageName: node + linkType: hard + +"keyv@npm:^4.5.4": + version: 4.5.4 + resolution: "keyv@npm:4.5.4" + dependencies: + json-buffer: "npm:3.0.1" + checksum: 10c0/aa52f3c5e18e16bb6324876bb8b59dd02acf782a4b789c7b2ae21107fab95fab3890ed448d4f8dba80ce05391eeac4bfabb4f02a20221342982f806fa2cf271e + languageName: node + linkType: hard + +"keyv@npm:^5.6.0": + version: 5.6.0 + resolution: "keyv@npm:5.6.0" + dependencies: + "@keyv/serialize": "npm:^1.1.1" + checksum: 10c0/c3ea795b6e03593ca57c8f70928a69bad14c13389a7fb75649a115ff55615244b04d8902798d841c17f0bb4a8a8866c97133b543b93f151b440170bba09176db + languageName: node + linkType: hard + +"kruptein@npm:^2.0.4": + version: 2.2.3 + resolution: "kruptein@npm:2.2.3" + dependencies: + asn1.js: "npm:^5.4.1" + checksum: 10c0/bb49bf80eda500bacd5e2f934ee6edb347559719908b81a712c86db75c8a14109ba66796f906323ae68b04d687bccc2c057208ca96cad576d3d5cb0281ea20dd + languageName: node + linkType: hard + +"levn@npm:^0.4.1": + version: 0.4.1 + resolution: "levn@npm:0.4.1" + dependencies: + prelude-ls: "npm:^1.2.1" + type-check: "npm:~0.4.0" + checksum: 10c0/effb03cad7c89dfa5bd4f6989364bfc79994c2042ec5966cb9b95990e2edee5cd8969ddf42616a0373ac49fac1403437deaf6e9050fbbaa3546093a59b9ac94e + languageName: node + linkType: hard + +"lightningcss-android-arm64@npm:1.32.0": + version: 1.32.0 + resolution: "lightningcss-android-arm64@npm:1.32.0" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"lightningcss-darwin-arm64@npm:1.32.0": + version: 1.32.0 + resolution: "lightningcss-darwin-arm64@npm:1.32.0" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"lightningcss-darwin-x64@npm:1.32.0": + version: 1.32.0 + resolution: "lightningcss-darwin-x64@npm:1.32.0" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"lightningcss-freebsd-x64@npm:1.32.0": + version: 1.32.0 + resolution: "lightningcss-freebsd-x64@npm:1.32.0" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"lightningcss-linux-arm-gnueabihf@npm:1.32.0": + version: 1.32.0 + resolution: "lightningcss-linux-arm-gnueabihf@npm:1.32.0" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"lightningcss-linux-arm64-gnu@npm:1.32.0": + version: 1.32.0 + resolution: "lightningcss-linux-arm64-gnu@npm:1.32.0" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"lightningcss-linux-arm64-musl@npm:1.32.0": + version: 1.32.0 + resolution: "lightningcss-linux-arm64-musl@npm:1.32.0" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"lightningcss-linux-x64-gnu@npm:1.32.0": + version: 1.32.0 + resolution: "lightningcss-linux-x64-gnu@npm:1.32.0" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"lightningcss-linux-x64-musl@npm:1.32.0": + version: 1.32.0 + resolution: "lightningcss-linux-x64-musl@npm:1.32.0" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"lightningcss-win32-arm64-msvc@npm:1.32.0": + version: 1.32.0 + resolution: "lightningcss-win32-arm64-msvc@npm:1.32.0" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"lightningcss-win32-x64-msvc@npm:1.32.0": + version: 1.32.0 + resolution: "lightningcss-win32-x64-msvc@npm:1.32.0" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"lightningcss@npm:^1.32.0": + version: 1.32.0 + resolution: "lightningcss@npm:1.32.0" + dependencies: + detect-libc: "npm:^2.0.3" + lightningcss-android-arm64: "npm:1.32.0" + lightningcss-darwin-arm64: "npm:1.32.0" + lightningcss-darwin-x64: "npm:1.32.0" + lightningcss-freebsd-x64: "npm:1.32.0" + lightningcss-linux-arm-gnueabihf: "npm:1.32.0" + lightningcss-linux-arm64-gnu: "npm:1.32.0" + lightningcss-linux-arm64-musl: "npm:1.32.0" + lightningcss-linux-x64-gnu: "npm:1.32.0" + lightningcss-linux-x64-musl: "npm:1.32.0" + lightningcss-win32-arm64-msvc: "npm:1.32.0" + lightningcss-win32-x64-msvc: "npm:1.32.0" + dependenciesMeta: + lightningcss-android-arm64: + optional: true + lightningcss-darwin-arm64: + optional: true + lightningcss-darwin-x64: + optional: true + lightningcss-freebsd-x64: + optional: true + lightningcss-linux-arm-gnueabihf: + optional: true + lightningcss-linux-arm64-gnu: + optional: true + lightningcss-linux-arm64-musl: + optional: true + lightningcss-linux-x64-gnu: + optional: true + lightningcss-linux-x64-musl: + optional: true + lightningcss-win32-arm64-msvc: + optional: true + lightningcss-win32-x64-msvc: + optional: true + checksum: 10c0/70945bd55097af46fc9fab7f5ed09cd5869d85940a2acab7ee06d0117004a1d68155708a2d462531cea2fc3c67aefc9333a7068c80b0b78dd404c16838809e03 + languageName: node + linkType: hard + +"link@npm:^2.1.2": + version: 2.1.2 + resolution: "link@npm:2.1.2" + bin: + link: dist/cli.js + checksum: 10c0/1b4a70eb649f36f2fc31210940cfddb38dfa7ba6096d29e3ee071276fe1f430356ed7cfe4eb941a1b3f4192c7a691857c329492eaa668900e72fbfd95578c49e + languageName: node + linkType: hard + +"locate-path@npm:^6.0.0": + version: 6.0.0 + resolution: "locate-path@npm:6.0.0" + dependencies: + p-locate: "npm:^5.0.0" + checksum: 10c0/d3972ab70dfe58ce620e64265f90162d247e87159b6126b01314dd67be43d50e96a50b517bce2d9452a79409c7614054c277b5232377de50416564a77ac7aad3 + languageName: node + linkType: hard + +"lodash.includes@npm:^4.3.0": + version: 4.3.0 + resolution: "lodash.includes@npm:4.3.0" + checksum: 10c0/7ca498b9b75bf602d04e48c0adb842dfc7d90f77bcb2a91a2b2be34a723ad24bc1c8b3683ec6b2552a90f216c723cdea530ddb11a3320e08fa38265703978f4b + languageName: node + linkType: hard + +"lodash.isboolean@npm:^3.0.3": + version: 3.0.3 + resolution: "lodash.isboolean@npm:3.0.3" + checksum: 10c0/0aac604c1ef7e72f9a6b798e5b676606042401dd58e49f051df3cc1e3adb497b3d7695635a5cbec4ae5f66456b951fdabe7d6b387055f13267cde521f10ec7f7 + languageName: node + linkType: hard + +"lodash.isinteger@npm:^4.0.4": + version: 4.0.4 + resolution: "lodash.isinteger@npm:4.0.4" + checksum: 10c0/4c3e023a2373bf65bf366d3b8605b97ec830bca702a926939bcaa53f8e02789b6a176e7f166b082f9365bfec4121bfeb52e86e9040cb8d450e64c858583f61b7 + languageName: node + linkType: hard + +"lodash.isnumber@npm:^3.0.3": + version: 3.0.3 + resolution: "lodash.isnumber@npm:3.0.3" + checksum: 10c0/2d01530513a1ee4f72dd79528444db4e6360588adcb0e2ff663db2b3f642d4bb3d687051ae1115751ca9082db4fdef675160071226ca6bbf5f0c123dbf0aa12d + languageName: node + linkType: hard + +"lodash.isplainobject@npm:^4.0.6": + version: 4.0.6 + resolution: "lodash.isplainobject@npm:4.0.6" + checksum: 10c0/afd70b5c450d1e09f32a737bed06ff85b873ecd3d3d3400458725283e3f2e0bb6bf48e67dbe7a309eb371a822b16a26cca4a63c8c52db3fc7dc9d5f9dd324cbb + languageName: node + linkType: hard + +"lodash.isstring@npm:^4.0.1": + version: 4.0.1 + resolution: "lodash.isstring@npm:4.0.1" + checksum: 10c0/09eaf980a283f9eef58ef95b30ec7fee61df4d6bf4aba3b5f096869cc58f24c9da17900febc8ffd67819b4e29de29793190e88dc96983db92d84c95fa85d1c92 + languageName: node + linkType: hard + +"lodash.once@npm:^4.0.0": + version: 4.1.1 + resolution: "lodash.once@npm:4.1.1" + checksum: 10c0/46a9a0a66c45dd812fcc016e46605d85ad599fe87d71a02f6736220554b52ffbe82e79a483ad40f52a8a95755b0d1077fba259da8bfb6694a7abbf4a48f1fc04 + languageName: node + linkType: hard + +"lru-cache@npm:^11.0.0, lru-cache@npm:^11.1.0, lru-cache@npm:^11.2.1": + version: 11.2.6 + resolution: "lru-cache@npm:11.2.6" + checksum: 10c0/73bbffb298760e71b2bfe8ebc16a311c6a60ceddbba919cfedfd8635c2d125fbfb5a39b71818200e67973b11f8d59c5a9e31d6f90722e340e90393663a66e5cd + languageName: node + linkType: hard + +"magic-string@npm:^0.30.21": + version: 0.30.21 + resolution: "magic-string@npm:0.30.21" + dependencies: + "@jridgewell/sourcemap-codec": "npm:^1.5.5" + checksum: 10c0/299378e38f9a270069fc62358522ddfb44e94244baa0d6a8980ab2a9b2490a1d03b236b447eee309e17eb3bddfa482c61259d47960eb018a904f0ded52780c4a + languageName: node + linkType: hard + +"make-fetch-happen@npm:^15.0.0": + version: 15.0.3 + resolution: "make-fetch-happen@npm:15.0.3" + dependencies: + "@npmcli/agent": "npm:^4.0.0" + cacache: "npm:^20.0.1" + http-cache-semantics: "npm:^4.1.1" + minipass: "npm:^7.0.2" + minipass-fetch: "npm:^5.0.0" + minipass-flush: "npm:^1.0.5" + minipass-pipeline: "npm:^1.2.4" + negotiator: "npm:^1.0.0" + proc-log: "npm:^6.0.0" + promise-retry: "npm:^2.0.1" + ssri: "npm:^13.0.0" + checksum: 10c0/525f74915660be60b616bcbd267c4a5b59481b073ba125e45c9c3a041bb1a47a2bd0ae79d028eb6f5f95bf9851a4158423f5068539c3093621abb64027e8e461 + languageName: node + linkType: hard + +"math-intrinsics@npm:^1.1.0": + version: 1.1.0 + resolution: "math-intrinsics@npm:1.1.0" + checksum: 10c0/7579ff94e899e2f76ab64491d76cf606274c874d8f2af4a442c016bd85688927fcfca157ba6bf74b08e9439dc010b248ce05b96cc7c126a354c3bae7fcb48b7f + languageName: node + linkType: hard + +"media-typer@npm:^1.1.0": + version: 1.1.0 + resolution: "media-typer@npm:1.1.0" + checksum: 10c0/7b4baa40b25964bb90e2121ee489ec38642127e48d0cc2b6baa442688d3fde6262bfdca86d6bbf6ba708784afcac168c06840c71facac70e390f5f759ac121b9 + languageName: node + linkType: hard + +"merge-descriptors@npm:^2.0.0": + version: 2.0.0 + resolution: "merge-descriptors@npm:2.0.0" + checksum: 10c0/95389b7ced3f9b36fbdcf32eb946dc3dd1774c2fdf164609e55b18d03aa499b12bd3aae3a76c1c7185b96279e9803525550d3eb292b5224866060a288f335cb3 + languageName: node + linkType: hard + +"methods@npm:^1.1.2": + version: 1.1.2 + resolution: "methods@npm:1.1.2" + checksum: 10c0/bdf7cc72ff0a33e3eede03708c08983c4d7a173f91348b4b1e4f47d4cdbf734433ad971e7d1e8c77247d9e5cd8adb81ea4c67b0a2db526b758b2233d7814b8b2 + languageName: node + linkType: hard + +"mime-db@npm:1.52.0": + version: 1.52.0 + resolution: "mime-db@npm:1.52.0" + checksum: 10c0/0557a01deebf45ac5f5777fe7740b2a5c309c6d62d40ceab4e23da9f821899ce7a900b7ac8157d4548ddbb7beffe9abc621250e6d182b0397ec7f10c7b91a5aa + languageName: node + linkType: hard + +"mime-db@npm:^1.54.0": + version: 1.54.0 + resolution: "mime-db@npm:1.54.0" + checksum: 10c0/8d907917bc2a90fa2df842cdf5dfeaf509adc15fe0531e07bb2f6ab15992416479015828d6a74200041c492e42cce3ebf78e5ce714388a0a538ea9c53eece284 + languageName: node + linkType: hard + +"mime-types@npm:^2.1.12": + version: 2.1.35 + resolution: "mime-types@npm:2.1.35" + dependencies: + mime-db: "npm:1.52.0" + checksum: 10c0/82fb07ec56d8ff1fc999a84f2f217aa46cb6ed1033fefaabd5785b9a974ed225c90dc72fff460259e66b95b73648596dbcc50d51ed69cdf464af2d237d3149b2 + languageName: node + linkType: hard + +"mime-types@npm:^3.0.0, mime-types@npm:^3.0.2": + version: 3.0.2 + resolution: "mime-types@npm:3.0.2" + dependencies: + mime-db: "npm:^1.54.0" + checksum: 10c0/35a0dd1035d14d185664f346efcdb72e93ef7a9b6e9ae808bd1f6358227010267fab52657b37562c80fc888ff76becb2b2938deb5e730818b7983bf8bd359767 + languageName: node + linkType: hard + +"mime@npm:2.6.0": + version: 2.6.0 + resolution: "mime@npm:2.6.0" + bin: + mime: cli.js + checksum: 10c0/a7f2589900d9c16e3bdf7672d16a6274df903da958c1643c9c45771f0478f3846dcb1097f31eb9178452570271361e2149310931ec705c037210fc69639c8e6c + languageName: node + linkType: hard + +"minimalistic-assert@npm:^1.0.0": + version: 1.0.1 + resolution: "minimalistic-assert@npm:1.0.1" + checksum: 10c0/96730e5601cd31457f81a296f521eb56036e6f69133c0b18c13fe941109d53ad23a4204d946a0d638d7f3099482a0cec8c9bb6d642604612ce43ee536be3dddd + languageName: node + linkType: hard + +"minimatch@npm:^10.2.2": + version: 10.2.2 + resolution: "minimatch@npm:10.2.2" + dependencies: + brace-expansion: "npm:^5.0.2" + checksum: 10c0/098831f2f542cb802e1f249c809008a016e1fef6b3a9eda9cf9ecb2b3d7979083951bd47c0c82fcf34330bd3b36638a493d4fa8e24cce58caf5b481de0f4e238 + languageName: node + linkType: hard + +"minimatch@npm:^10.2.4": + version: 10.2.4 + resolution: "minimatch@npm:10.2.4" + dependencies: + brace-expansion: "npm:^5.0.2" + checksum: 10c0/35f3dfb7b99b51efd46afd378486889f590e7efb10e0f6a10ba6800428cf65c9a8dedb74427d0570b318d749b543dc4e85f06d46d2858bc8cac7e1eb49a95945 + languageName: node + linkType: hard + +"minipass-collect@npm:^2.0.1": + version: 2.0.1 + resolution: "minipass-collect@npm:2.0.1" + dependencies: + minipass: "npm:^7.0.3" + checksum: 10c0/5167e73f62bb74cc5019594709c77e6a742051a647fe9499abf03c71dca75515b7959d67a764bdc4f8b361cf897fbf25e2d9869ee039203ed45240f48b9aa06e + languageName: node + linkType: hard + +"minipass-fetch@npm:^5.0.0": + version: 5.0.1 + resolution: "minipass-fetch@npm:5.0.1" + dependencies: + encoding: "npm:^0.1.13" + minipass: "npm:^7.0.3" + minipass-sized: "npm:^2.0.0" + minizlib: "npm:^3.0.1" + dependenciesMeta: + encoding: + optional: true + checksum: 10c0/50bcf48c9841ebb25e29a2817468595219c72cfffc7c175a1d7327843c8bef9b72cb01778f46df7eca695dfe47ab98e6167af4cb026ddd80f660842919a5193c + languageName: node + linkType: hard + +"minipass-flush@npm:^1.0.5": + version: 1.0.5 + resolution: "minipass-flush@npm:1.0.5" + dependencies: + minipass: "npm:^3.0.0" + checksum: 10c0/2a51b63feb799d2bb34669205eee7c0eaf9dce01883261a5b77410c9408aa447e478efd191b4de6fc1101e796ff5892f8443ef20d9544385819093dbb32d36bd + languageName: node + linkType: hard + +"minipass-pipeline@npm:^1.2.4": + version: 1.2.4 + resolution: "minipass-pipeline@npm:1.2.4" + dependencies: + minipass: "npm:^3.0.0" + checksum: 10c0/cbda57cea20b140b797505dc2cac71581a70b3247b84480c1fed5ca5ba46c25ecc25f68bfc9e6dcb1a6e9017dab5c7ada5eab73ad4f0a49d84e35093e0c643f2 + languageName: node + linkType: hard + +"minipass-sized@npm:^2.0.0": + version: 2.0.0 + resolution: "minipass-sized@npm:2.0.0" + dependencies: + minipass: "npm:^7.1.2" + checksum: 10c0/f9201696a6f6d68610d04c9c83e3d2e5cb9c026aae1c8cbf7e17f386105cb79c1bb088dbc21bf0b1eb4f3fb5df384fd1e7aa3bf1f33868c416ae8c8a92679db8 + languageName: node + linkType: hard + +"minipass@npm:^3.0.0": + version: 3.3.6 + resolution: "minipass@npm:3.3.6" + dependencies: + yallist: "npm:^4.0.0" + checksum: 10c0/a114746943afa1dbbca8249e706d1d38b85ed1298b530f5808ce51f8e9e941962e2a5ad2e00eae7dd21d8a4aae6586a66d4216d1a259385e9d0358f0c1eba16c + languageName: node + linkType: hard + +"minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4, minipass@npm:^7.1.2, minipass@npm:^7.1.3": + version: 7.1.3 + resolution: "minipass@npm:7.1.3" + checksum: 10c0/539da88daca16533211ea5a9ee98dc62ff5742f531f54640dd34429e621955e91cc280a91a776026264b7f9f6735947629f920944e9c1558369e8bf22eb33fbb + languageName: node + linkType: hard + +"minizlib@npm:^3.0.1, minizlib@npm:^3.1.0": + version: 3.1.0 + resolution: "minizlib@npm:3.1.0" + dependencies: + minipass: "npm:^7.1.2" + checksum: 10c0/5aad75ab0090b8266069c9aabe582c021ae53eb33c6c691054a13a45db3b4f91a7fb1bd79151e6b4e9e9a86727b522527c0a06ec7d45206b745d54cd3097bcec + languageName: node + linkType: hard + +"ms@npm:2.0.0": + version: 2.0.0 + resolution: "ms@npm:2.0.0" + checksum: 10c0/f8fda810b39fd7255bbdc451c46286e549794fcc700dc9cd1d25658bbc4dc2563a5de6fe7c60f798a16a60c6ceb53f033cb353f493f0cf63e5199b702943159d + languageName: node + linkType: hard + +"ms@npm:^2.1.1, ms@npm:^2.1.3": + version: 2.1.3 + resolution: "ms@npm:2.1.3" + checksum: 10c0/d924b57e7312b3b63ad21fc5b3dc0af5e78d61a1fc7cfb5457edaf26326bf62be5307cc87ffb6862ef1c2b33b0233cdb5d4f01c4c958cc0d660948b65a287a48 + languageName: node + linkType: hard + +"nanoid@npm:^3.3.11": + version: 3.3.11 + resolution: "nanoid@npm:3.3.11" + bin: + nanoid: bin/nanoid.cjs + checksum: 10c0/40e7f70b3d15f725ca072dfc4f74e81fcf1fbb02e491cf58ac0c79093adc9b0a73b152bcde57df4b79cd097e13023d7504acb38404a4da7bc1cd8e887b82fe0b + languageName: node + linkType: hard + +"natural-compare@npm:^1.4.0": + version: 1.4.0 + resolution: "natural-compare@npm:1.4.0" + checksum: 10c0/f5f9a7974bfb28a91afafa254b197f0f22c684d4a1731763dda960d2c8e375b36c7d690e0d9dc8fba774c537af14a7e979129bca23d88d052fbeb9466955e447 + languageName: node + linkType: hard + +"negotiator@npm:^1.0.0": + version: 1.0.0 + resolution: "negotiator@npm:1.0.0" + checksum: 10c0/4c559dd52669ea48e1914f9d634227c561221dd54734070791f999c52ed0ff36e437b2e07d5c1f6e32909fc625fe46491c16e4a8f0572567d4dd15c3a4fda04b + languageName: node + linkType: hard + +"node-gyp@npm:latest": + version: 12.2.0 + resolution: "node-gyp@npm:12.2.0" + dependencies: + env-paths: "npm:^2.2.0" + exponential-backoff: "npm:^3.1.1" + graceful-fs: "npm:^4.2.6" + make-fetch-happen: "npm:^15.0.0" + nopt: "npm:^9.0.0" + proc-log: "npm:^6.0.0" + semver: "npm:^7.3.5" + tar: "npm:^7.5.4" + tinyglobby: "npm:^0.2.12" + which: "npm:^6.0.0" + bin: + node-gyp: bin/node-gyp.js + checksum: 10c0/3ed046746a5a7d90950cd8b0547332b06598443f31fe213ef4332a7174c7b7d259e1704835feda79b87d3f02e59d7791842aac60642ede4396ab25fdf0f8f759 + languageName: node + linkType: hard + +"nopt@npm:^9.0.0": + version: 9.0.0 + resolution: "nopt@npm:9.0.0" + dependencies: + abbrev: "npm:^4.0.0" + bin: + nopt: bin/nopt.js + checksum: 10c0/1822eb6f9b020ef6f7a7516d7b64a8036e09666ea55ac40416c36e4b2b343122c3cff0e2f085675f53de1d2db99a2a89a60ccea1d120bcd6a5347bf6ceb4a7fd + languageName: node + linkType: hard + +"object-assign@npm:^4.1.1": + version: 4.1.1 + resolution: "object-assign@npm:4.1.1" + checksum: 10c0/1f4df9945120325d041ccf7b86f31e8bcc14e73d29171e37a7903050e96b81323784ec59f93f102ec635bcf6fa8034ba3ea0a8c7e69fa202b87ae3b6cec5a414 + languageName: node + linkType: hard + +"object-inspect@npm:^1.13.3": + version: 1.13.4 + resolution: "object-inspect@npm:1.13.4" + checksum: 10c0/d7f8711e803b96ea3191c745d6f8056ce1f2496e530e6a19a0e92d89b0fa3c76d910c31f0aa270432db6bd3b2f85500a376a83aaba849a8d518c8845b3211692 + languageName: node + linkType: hard + +"obug@npm:^2.1.1": + version: 2.1.1 + resolution: "obug@npm:2.1.1" + checksum: 10c0/59dccd7de72a047e08f8649e94c1015ec72f94eefb6ddb57fb4812c4b425a813bc7e7cd30c9aca20db3c59abc3c85cc7a62bb656a968741d770f4e8e02bc2e78 + languageName: node + linkType: hard + +"on-finished@npm:^2.4.1": + version: 2.4.1 + resolution: "on-finished@npm:2.4.1" + dependencies: + ee-first: "npm:1.1.1" + checksum: 10c0/46fb11b9063782f2d9968863d9cbba33d77aa13c17f895f56129c274318b86500b22af3a160fe9995aa41317efcd22941b6eba747f718ced08d9a73afdb087b4 + languageName: node + linkType: hard + +"on-headers@npm:~1.1.0": + version: 1.1.0 + resolution: "on-headers@npm:1.1.0" + checksum: 10c0/2c3b6b0d68ec9adbd561dc2d61c9b14da8ac03d8a2f0fd9e97bdf0600c887d5d97f664ff3be6876cf40cda6e3c587d73a4745e10b426ac50c7664fc5a0dfc0a1 + languageName: node + linkType: hard + +"once@npm:^1.4.0": + version: 1.4.0 + resolution: "once@npm:1.4.0" + dependencies: + wrappy: "npm:1" + checksum: 10c0/5d48aca287dfefabd756621c5dfce5c91a549a93e9fdb7b8246bc4c4790aa2ec17b34a260530474635147aeb631a2dcc8b32c613df0675f96041cbb8244517d0 + languageName: node + linkType: hard + +"optionator@npm:^0.9.3": + version: 0.9.4 + resolution: "optionator@npm:0.9.4" + dependencies: + deep-is: "npm:^0.1.3" + fast-levenshtein: "npm:^2.0.6" + levn: "npm:^0.4.1" + prelude-ls: "npm:^1.2.1" + type-check: "npm:^0.4.0" + word-wrap: "npm:^1.2.5" + checksum: 10c0/4afb687a059ee65b61df74dfe87d8d6815cd6883cb8b3d5883a910df72d0f5d029821f37025e4bccf4048873dbdb09acc6d303d27b8f76b1a80dd5a7d5334675 + languageName: node + linkType: hard + +"p-limit@npm:^3.0.2": + version: 3.1.0 + resolution: "p-limit@npm:3.1.0" + dependencies: + yocto-queue: "npm:^0.1.0" + checksum: 10c0/9db675949dbdc9c3763c89e748d0ef8bdad0afbb24d49ceaf4c46c02c77d30db4e0652ed36d0a0a7a95154335fab810d95c86153105bb73b3a90448e2bb14e1a + languageName: node + linkType: hard + +"p-locate@npm:^5.0.0": + version: 5.0.0 + resolution: "p-locate@npm:5.0.0" + dependencies: + p-limit: "npm:^3.0.2" + checksum: 10c0/2290d627ab7903b8b70d11d384fee714b797f6040d9278932754a6860845c4d3190603a0772a663c8cb5a7b21d1b16acb3a6487ebcafa9773094edc3dfe6009a + languageName: node + linkType: hard + +"p-map@npm:^7.0.2": + version: 7.0.4 + resolution: "p-map@npm:7.0.4" + checksum: 10c0/a5030935d3cb2919d7e89454d1ce82141e6f9955413658b8c9403cfe379283770ed3048146b44cde168aa9e8c716505f196d5689db0ae3ce9a71521a2fef3abd + languageName: node + linkType: hard + +"parseurl@npm:^1.3.3, parseurl@npm:~1.3.3": + version: 1.3.3 + resolution: "parseurl@npm:1.3.3" + checksum: 10c0/90dd4760d6f6174adb9f20cf0965ae12e23879b5f5464f38e92fce8073354341e4b3b76fa3d878351efe7d01e617121955284cfd002ab087fba1a0726ec0b4f5 + languageName: node + linkType: hard + +"path-exists@npm:^4.0.0": + version: 4.0.0 + resolution: "path-exists@npm:4.0.0" + checksum: 10c0/8c0bd3f5238188197dc78dced15207a4716c51cc4e3624c44fc97acf69558f5ebb9a2afff486fe1b4ee148e0c133e96c5e11a9aa5c48a3006e3467da070e5e1b + languageName: node + linkType: hard + +"path-key@npm:^3.1.0": + version: 3.1.1 + resolution: "path-key@npm:3.1.1" + checksum: 10c0/748c43efd5a569c039d7a00a03b58eecd1d75f3999f5a28303d75f521288df4823bc057d8784eb72358b2895a05f29a070bc9f1f17d28226cc4e62494cc58c4c + languageName: node + linkType: hard + +"path-scurry@npm:^2.0.2": + version: 2.0.2 + resolution: "path-scurry@npm:2.0.2" + dependencies: + lru-cache: "npm:^11.0.0" + minipass: "npm:^7.1.2" + checksum: 10c0/b35ad37cf6557a87fd057121ce2be7695380c9138d93e87ae928609da259ea0a170fac6f3ef1eb3ece8a068e8b7f2f3adf5bb2374cf4d4a57fe484954fcc9482 + languageName: node + linkType: hard + +"path-to-regexp@npm:^8.0.0": + version: 8.3.0 + resolution: "path-to-regexp@npm:8.3.0" + checksum: 10c0/ee1544a73a3f294a97a4c663b0ce71bbf1621d732d80c9c9ed201b3e911a86cb628ebad691b9d40f40a3742fe22011e5a059d8eed2cf63ec2cb94f6fb4efe67c + languageName: node + linkType: hard + +"pathe@npm:^2.0.3": + version: 2.0.3 + resolution: "pathe@npm:2.0.3" + checksum: 10c0/c118dc5a8b5c4166011b2b70608762e260085180bb9e33e80a50dcdb1e78c010b1624f4280c492c92b05fc276715a4c357d1f9edc570f8f1b3d90b6839ebaca1 + languageName: node + linkType: hard + +"picocolors@npm:^1.1.1": + version: 1.1.1 + resolution: "picocolors@npm:1.1.1" + checksum: 10c0/e2e3e8170ab9d7c7421969adaa7e1b31434f789afb9b3f115f6b96d91945041ac3ceb02e9ec6fe6510ff036bcc0bf91e69a1772edc0b707e12b19c0f2d6bcf58 + languageName: node + linkType: hard + +"picomatch@npm:^4.0.3": + version: 4.0.3 + resolution: "picomatch@npm:4.0.3" + checksum: 10c0/9582c951e95eebee5434f59e426cddd228a7b97a0161a375aed4be244bd3fe8e3a31b846808ea14ef2c8a2527a6eeab7b3946a67d5979e81694654f939473ae2 + languageName: node + linkType: hard + +"postcss@npm:^8.5.8": + version: 8.5.8 + resolution: "postcss@npm:8.5.8" + dependencies: + nanoid: "npm:^3.3.11" + picocolors: "npm:^1.1.1" + source-map-js: "npm:^1.2.1" + checksum: 10c0/dd918f7127ee7c60a0295bae2e72b3787892296e1d1c3c564d7a2a00c68d8df83cadc3178491259daa19ccc54804fb71ed8c937c6787e08d8bd4bedf8d17044c + languageName: node + linkType: hard + +"prelude-ls@npm:^1.2.1": + version: 1.2.1 + resolution: "prelude-ls@npm:1.2.1" + checksum: 10c0/b00d617431e7886c520a6f498a2e14c75ec58f6d93ba48c3b639cf241b54232d90daa05d83a9e9b9fef6baa63cb7e1e4602c2372fea5bc169668401eb127d0cd + languageName: node + linkType: hard + +"prettier-linter-helpers@npm:^1.0.1": + version: 1.0.1 + resolution: "prettier-linter-helpers@npm:1.0.1" + dependencies: + fast-diff: "npm:^1.1.2" + checksum: 10c0/91cea965681bc5f62c9d26bd3ca6358b81557261d4802e96ec1cf0acbd99d4b61632d53320cd2c3ec7d7f7805a81345644108a41ef46ddc9688e783a9ac792d1 + languageName: node + linkType: hard + +"prettier@npm:^3.8.1": + version: 3.8.1 + resolution: "prettier@npm:3.8.1" + bin: + prettier: bin/prettier.cjs + checksum: 10c0/33169b594009e48f570471271be7eac7cdcf88a209eed39ac3b8d6d78984039bfa9132f82b7e6ba3b06711f3bfe0222a62a1bfb87c43f50c25a83df1b78a2c42 + languageName: node + linkType: hard + +"proc-log@npm:^6.0.0": + version: 6.1.0 + resolution: "proc-log@npm:6.1.0" + checksum: 10c0/4f178d4062733ead9d71a9b1ab24ebcecdfe2250916a5b1555f04fe2eda972a0ec76fbaa8df1ad9c02707add6749219d118a4fc46dc56bdfe4dde4b47d80bb82 + languageName: node + linkType: hard + +"promise-retry@npm:^2.0.1": + version: 2.0.1 + resolution: "promise-retry@npm:2.0.1" + dependencies: + err-code: "npm:^2.0.2" + retry: "npm:^0.12.0" + checksum: 10c0/9c7045a1a2928094b5b9b15336dcd2a7b1c052f674550df63cc3f36cd44028e5080448175b6f6ca32b642de81150f5e7b1a98b728f15cb069f2dd60ac2616b96 + languageName: node + linkType: hard + +"proxy-addr@npm:^2.0.7": + version: 2.0.7 + resolution: "proxy-addr@npm:2.0.7" + dependencies: + forwarded: "npm:0.2.0" + ipaddr.js: "npm:1.9.1" + checksum: 10c0/c3eed999781a35f7fd935f398b6d8920b6fb00bbc14287bc6de78128ccc1a02c89b95b56742bf7cf0362cc333c61d138532049c7dedc7a328ef13343eff81210 + languageName: node + linkType: hard + +"punycode@npm:^2.1.0": + version: 2.3.1 + resolution: "punycode@npm:2.3.1" + checksum: 10c0/14f76a8206bc3464f794fb2e3d3cc665ae416c01893ad7a02b23766eb07159144ee612ad67af5e84fa4479ccfe67678c4feb126b0485651b302babf66f04f9e9 + languageName: node + linkType: hard + +"qs@npm:^6.14.0, qs@npm:^6.14.1": + version: 6.15.0 + resolution: "qs@npm:6.15.0" + dependencies: + side-channel: "npm:^1.1.0" + checksum: 10c0/ff341078a78a991d8a48b4524d52949211447b4b1ad907f489cac0770cbc346a28e47304455c0320e5fb000f8762d64b03331e3b71865f663bf351bcba8cdb4b + languageName: node + linkType: hard + +"random-bytes@npm:~1.0.0": + version: 1.0.0 + resolution: "random-bytes@npm:1.0.0" + checksum: 10c0/71e7a600e0976e9ebc269793a0577d47b965fa678fcc9e9623e427f909d1b3669db5b3a178dbf61229f0724ea23dba64db389f0be0ba675c6a6b837c02f29b8f + languageName: node + linkType: hard + +"range-parser@npm:^1.2.1": + version: 1.2.1 + resolution: "range-parser@npm:1.2.1" + checksum: 10c0/96c032ac2475c8027b7a4e9fe22dc0dfe0f6d90b85e496e0f016fbdb99d6d066de0112e680805075bd989905e2123b3b3d002765149294dce0c1f7f01fcc2ea0 + languageName: node + linkType: hard + +"raw-body@npm:^3.0.1": + version: 3.0.2 + resolution: "raw-body@npm:3.0.2" + dependencies: + bytes: "npm:~3.1.2" + http-errors: "npm:~2.0.1" + iconv-lite: "npm:~0.7.0" + unpipe: "npm:~1.0.0" + checksum: 10c0/d266678d08e1e7abea62c0ce5864344e980fa81c64f6b481e9842c5beaed2cdcf975f658a3ccd67ad35fc919c1f6664ccc106067801850286a6cbe101de89f29 + languageName: node + linkType: hard + +"regexp-tree@npm:~0.1.1": + version: 0.1.27 + resolution: "regexp-tree@npm:0.1.27" + bin: + regexp-tree: bin/regexp-tree + checksum: 10c0/f636f44b4a0d93d7d6926585ecd81f63e4ce2ac895bc417b2ead0874cd36b337dcc3d0fedc63f69bf5aaeaa4340f36ca7e750c9687cceaf8087374e5284e843c + languageName: node + linkType: hard + +"resolve-pkg-maps@npm:^1.0.0": + version: 1.0.0 + resolution: "resolve-pkg-maps@npm:1.0.0" + checksum: 10c0/fb8f7bbe2ca281a73b7ef423a1cbc786fb244bd7a95cbe5c3fba25b27d327150beca8ba02f622baea65919a57e061eb5005204daa5f93ed590d9b77463a567ab + languageName: node + linkType: hard + +"retry@npm:^0.12.0": + version: 0.12.0 + resolution: "retry@npm:0.12.0" + checksum: 10c0/59933e8501727ba13ad73ef4a04d5280b3717fd650408460c987392efe9d7be2040778ed8ebe933c5cbd63da3dcc37919c141ef8af0a54a6e4fca5a2af177bfe + languageName: node + linkType: hard + +"rolldown@npm:1.0.0-rc.9": + version: 1.0.0-rc.9 + resolution: "rolldown@npm:1.0.0-rc.9" + dependencies: + "@oxc-project/types": "npm:=0.115.0" + "@rolldown/binding-android-arm64": "npm:1.0.0-rc.9" + "@rolldown/binding-darwin-arm64": "npm:1.0.0-rc.9" + "@rolldown/binding-darwin-x64": "npm:1.0.0-rc.9" + "@rolldown/binding-freebsd-x64": "npm:1.0.0-rc.9" + "@rolldown/binding-linux-arm-gnueabihf": "npm:1.0.0-rc.9" + "@rolldown/binding-linux-arm64-gnu": "npm:1.0.0-rc.9" + "@rolldown/binding-linux-arm64-musl": "npm:1.0.0-rc.9" + "@rolldown/binding-linux-ppc64-gnu": "npm:1.0.0-rc.9" + "@rolldown/binding-linux-s390x-gnu": "npm:1.0.0-rc.9" + "@rolldown/binding-linux-x64-gnu": "npm:1.0.0-rc.9" + "@rolldown/binding-linux-x64-musl": "npm:1.0.0-rc.9" + "@rolldown/binding-openharmony-arm64": "npm:1.0.0-rc.9" + "@rolldown/binding-wasm32-wasi": "npm:1.0.0-rc.9" + "@rolldown/binding-win32-arm64-msvc": "npm:1.0.0-rc.9" + "@rolldown/binding-win32-x64-msvc": "npm:1.0.0-rc.9" + "@rolldown/pluginutils": "npm:1.0.0-rc.9" + dependenciesMeta: + "@rolldown/binding-android-arm64": + optional: true + "@rolldown/binding-darwin-arm64": + optional: true + "@rolldown/binding-darwin-x64": + optional: true + "@rolldown/binding-freebsd-x64": + optional: true + "@rolldown/binding-linux-arm-gnueabihf": + optional: true + "@rolldown/binding-linux-arm64-gnu": + optional: true + "@rolldown/binding-linux-arm64-musl": + optional: true + "@rolldown/binding-linux-ppc64-gnu": + optional: true + "@rolldown/binding-linux-s390x-gnu": + optional: true + "@rolldown/binding-linux-x64-gnu": + optional: true + "@rolldown/binding-linux-x64-musl": + optional: true + "@rolldown/binding-openharmony-arm64": + optional: true + "@rolldown/binding-wasm32-wasi": + optional: true + "@rolldown/binding-win32-arm64-msvc": + optional: true + "@rolldown/binding-win32-x64-msvc": + optional: true + bin: + rolldown: bin/cli.mjs + checksum: 10c0/d19af14dccf569dc25c0c3c2f1142b7a6f7cec291d55bba80cea71099f89c6d634145bb1b6487626ddd41d578f183f7065ed68067e49d2b964ad6242693b0f79 + languageName: node + linkType: hard + +"router@npm:^2.2.0": + version: 2.2.0 + resolution: "router@npm:2.2.0" + dependencies: + debug: "npm:^4.4.0" + depd: "npm:^2.0.0" + is-promise: "npm:^4.0.0" + parseurl: "npm:^1.3.3" + path-to-regexp: "npm:^8.0.0" + checksum: 10c0/3279de7450c8eae2f6e095e9edacbdeec0abb5cb7249c6e719faa0db2dba43574b4fff5892d9220631c9abaff52dd3cad648cfea2aaace845e1a071915ac8867 + languageName: node + linkType: hard + +"safe-buffer@npm:^5.0.1, safe-buffer@npm:~5.2.1": + version: 5.2.1 + resolution: "safe-buffer@npm:5.2.1" + checksum: 10c0/6501914237c0a86e9675d4e51d89ca3c21ffd6a31642efeba25ad65720bce6921c9e7e974e5be91a786b25aa058b5303285d3c15dbabf983a919f5f630d349f3 + languageName: node + linkType: hard + +"safe-regex@npm:^2.1.1": + version: 2.1.1 + resolution: "safe-regex@npm:2.1.1" + dependencies: + regexp-tree: "npm:~0.1.1" + checksum: 10c0/53eb5d3ecf4b3c0954dff465eb179af4d2f5f77f74ba7b57489adbc4fa44454c3d391f37379cd28722d9ac6fa5b70be3f4645d4bd25df395fd99b934f6ec9265 + languageName: node + linkType: hard + +"safer-buffer@npm:>= 2.1.2 < 3.0.0, safer-buffer@npm:^2.1.0": + version: 2.1.2 + resolution: "safer-buffer@npm:2.1.2" + checksum: 10c0/7e3c8b2e88a1841c9671094bbaeebd94448111dd90a81a1f606f3f67708a6ec57763b3b47f06da09fc6054193e0e6709e77325415dc8422b04497a8070fa02d4 + languageName: node + linkType: hard + +"semver@npm:^7.3.5, semver@npm:^7.5.4": + version: 7.7.4 + resolution: "semver@npm:7.7.4" + bin: + semver: bin/semver.js + checksum: 10c0/5215ad0234e2845d4ea5bb9d836d42b03499546ddafb12075566899fc617f68794bb6f146076b6881d755de17d6c6cc73372555879ec7dce2c2feee947866ad2 + languageName: node + linkType: hard + +"send@npm:^1.1.0, send@npm:^1.2.0": + version: 1.2.1 + resolution: "send@npm:1.2.1" + dependencies: + debug: "npm:^4.4.3" + encodeurl: "npm:^2.0.0" + escape-html: "npm:^1.0.3" + etag: "npm:^1.8.1" + fresh: "npm:^2.0.0" + http-errors: "npm:^2.0.1" + mime-types: "npm:^3.0.2" + ms: "npm:^2.1.3" + on-finished: "npm:^2.4.1" + range-parser: "npm:^1.2.1" + statuses: "npm:^2.0.2" + checksum: 10c0/fbbbbdc902a913d65605274be23f3d604065cfc3ee3d78bf9fc8af1dc9fc82667c50d3d657f5e601ac657bac9b396b50ee97bd29cd55436320cf1cddebdcec72 + languageName: node + linkType: hard + +"serve-static@npm:^2.2.0": + version: 2.2.1 + resolution: "serve-static@npm:2.2.1" + dependencies: + encodeurl: "npm:^2.0.0" + escape-html: "npm:^1.0.3" + parseurl: "npm:^1.3.3" + send: "npm:^1.2.0" + checksum: 10c0/37986096e8572e2dfaad35a3925fa8da0c0969f8814fd7788e84d4d388bc068cf0c06d1658509788e55bed942a6b6d040a8a267fa92bb9ffb1179f8bacde5fd7 + languageName: node + linkType: hard + +"session-file-store@npm:^1.5.0": + version: 1.5.0 + resolution: "session-file-store@npm:1.5.0" + dependencies: + bagpipe: "npm:^0.3.5" + fs-extra: "npm:^8.0.1" + kruptein: "npm:^2.0.4" + object-assign: "npm:^4.1.1" + retry: "npm:^0.12.0" + write-file-atomic: "npm:3.0.3" + checksum: 10c0/85985e949588a5633d970a00a506931b4605fbaa546c494b253e462807179d213965bb4f75385d9cdf8f17af8fa00fab3c5822daa6f9ebb58038a1527c4cfbdd + languageName: node + linkType: hard + +"setprototypeof@npm:~1.2.0": + version: 1.2.0 + resolution: "setprototypeof@npm:1.2.0" + checksum: 10c0/68733173026766fa0d9ecaeb07f0483f4c2dc70ca376b3b7c40b7cda909f94b0918f6c5ad5ce27a9160bdfb475efaa9d5e705a11d8eaae18f9835d20976028bc + languageName: node + linkType: hard + +"shebang-command@npm:^2.0.0": + version: 2.0.0 + resolution: "shebang-command@npm:2.0.0" + dependencies: + shebang-regex: "npm:^3.0.0" + checksum: 10c0/a41692e7d89a553ef21d324a5cceb5f686d1f3c040759c50aab69688634688c5c327f26f3ecf7001ebfd78c01f3c7c0a11a7c8bfd0a8bc9f6240d4f40b224e4e + languageName: node + linkType: hard + +"shebang-regex@npm:^3.0.0": + version: 3.0.0 + resolution: "shebang-regex@npm:3.0.0" + checksum: 10c0/1dbed0726dd0e1152a92696c76c7f06084eb32a90f0528d11acd764043aacf76994b2fb30aa1291a21bd019d6699164d048286309a278855ee7bec06cf6fb690 + languageName: node + linkType: hard + +"side-channel-list@npm:^1.0.0": + version: 1.0.0 + resolution: "side-channel-list@npm:1.0.0" + dependencies: + es-errors: "npm:^1.3.0" + object-inspect: "npm:^1.13.3" + checksum: 10c0/644f4ac893456c9490ff388bf78aea9d333d5e5bfc64cfb84be8f04bf31ddc111a8d4b83b85d7e7e8a7b845bc185a9ad02c052d20e086983cf59f0be517d9b3d + languageName: node + linkType: hard + +"side-channel-map@npm:^1.0.1": + version: 1.0.1 + resolution: "side-channel-map@npm:1.0.1" + dependencies: + call-bound: "npm:^1.0.2" + es-errors: "npm:^1.3.0" + get-intrinsic: "npm:^1.2.5" + object-inspect: "npm:^1.13.3" + checksum: 10c0/010584e6444dd8a20b85bc926d934424bd809e1a3af941cace229f7fdcb751aada0fb7164f60c2e22292b7fa3c0ff0bce237081fd4cdbc80de1dc68e95430672 + languageName: node + linkType: hard + +"side-channel-weakmap@npm:^1.0.2": + version: 1.0.2 + resolution: "side-channel-weakmap@npm:1.0.2" + dependencies: + call-bound: "npm:^1.0.2" + es-errors: "npm:^1.3.0" + get-intrinsic: "npm:^1.2.5" + object-inspect: "npm:^1.13.3" + side-channel-map: "npm:^1.0.1" + checksum: 10c0/71362709ac233e08807ccd980101c3e2d7efe849edc51455030327b059f6c4d292c237f94dc0685031dd11c07dd17a68afde235d6cf2102d949567f98ab58185 + languageName: node + linkType: hard + +"side-channel@npm:^1.1.0": + version: 1.1.0 + resolution: "side-channel@npm:1.1.0" + dependencies: + es-errors: "npm:^1.3.0" + object-inspect: "npm:^1.13.3" + side-channel-list: "npm:^1.0.0" + side-channel-map: "npm:^1.0.1" + side-channel-weakmap: "npm:^1.0.2" + checksum: 10c0/cb20dad41eb032e6c24c0982e1e5a24963a28aa6122b4f05b3f3d6bf8ae7fd5474ef382c8f54a6a3ab86e0cac4d41a23bd64ede3970e5bfb50326ba02a7996e6 + languageName: node + linkType: hard + +"siginfo@npm:^2.0.0": + version: 2.0.0 + resolution: "siginfo@npm:2.0.0" + checksum: 10c0/3def8f8e516fbb34cb6ae415b07ccc5d9c018d85b4b8611e3dc6f8be6d1899f693a4382913c9ed51a06babb5201639d76453ab297d1c54a456544acf5c892e34 + languageName: node + linkType: hard + +"signal-exit@npm:^3.0.2": + version: 3.0.7 + resolution: "signal-exit@npm:3.0.7" + checksum: 10c0/25d272fa73e146048565e08f3309d5b942c1979a6f4a58a8c59d5fa299728e9c2fcd1a759ec870863b1fd38653670240cd420dad2ad9330c71f36608a6a1c912 + languageName: node + linkType: hard + +"smart-buffer@npm:^4.2.0": + version: 4.2.0 + resolution: "smart-buffer@npm:4.2.0" + checksum: 10c0/a16775323e1404dd43fabafe7460be13a471e021637bc7889468eb45ce6a6b207261f454e4e530a19500cc962c4cc5348583520843b363f4193cee5c00e1e539 + languageName: node + linkType: hard + +"socks-proxy-agent@npm:^8.0.3": + version: 8.0.5 + resolution: "socks-proxy-agent@npm:8.0.5" + dependencies: + agent-base: "npm:^7.1.2" + debug: "npm:^4.3.4" + socks: "npm:^2.8.3" + checksum: 10c0/5d2c6cecba6821389aabf18728325730504bf9bb1d9e342e7987a5d13badd7a98838cc9a55b8ed3cb866ad37cc23e1086f09c4d72d93105ce9dfe76330e9d2a6 + languageName: node + linkType: hard + +"socks@npm:^2.8.3": + version: 2.8.7 + resolution: "socks@npm:2.8.7" + dependencies: + ip-address: "npm:^10.0.1" + smart-buffer: "npm:^4.2.0" + checksum: 10c0/2805a43a1c4bcf9ebf6e018268d87b32b32b06fbbc1f9282573583acc155860dc361500f89c73bfbb157caa1b4ac78059eac0ef15d1811eb0ca75e0bdadbc9d2 + languageName: node + linkType: hard + +"source-map-js@npm:^1.2.1": + version: 1.2.1 + resolution: "source-map-js@npm:1.2.1" + checksum: 10c0/7bda1fc4c197e3c6ff17de1b8b2c20e60af81b63a52cb32ec5a5d67a20a7d42651e2cb34ebe93833c5a2a084377e17455854fee3e21e7925c64a51b6a52b0faf + languageName: node + linkType: hard + +"ssri@npm:^13.0.0": + version: 13.0.1 + resolution: "ssri@npm:13.0.1" + dependencies: + minipass: "npm:^7.0.3" + checksum: 10c0/cf6408a18676c57ff2ed06b8a20dc64bb3e748e5c7e095332e6aecaa2b8422b1e94a739a8453bf65156a8a47afe23757ba4ab52d3ea3b62322dc40875763e17a + languageName: node + linkType: hard + +"stackback@npm:0.0.2": + version: 0.0.2 + resolution: "stackback@npm:0.0.2" + checksum: 10c0/89a1416668f950236dd5ac9f9a6b2588e1b9b62b1b6ad8dff1bfc5d1a15dbf0aafc9b52d2226d00c28dffff212da464eaeebfc6b7578b9d180cef3e3782c5983 + languageName: node + linkType: hard + +"statuses@npm:^2.0.1, statuses@npm:^2.0.2, statuses@npm:~2.0.2": + version: 2.0.2 + resolution: "statuses@npm:2.0.2" + checksum: 10c0/a9947d98ad60d01f6b26727570f3bcceb6c8fa789da64fe6889908fe2e294d57503b14bf2b5af7605c2d36647259e856635cd4c49eab41667658ec9d0080ec3f + languageName: node + linkType: hard + +"std-env@npm:^4.0.0-rc.1": + version: 4.0.0 + resolution: "std-env@npm:4.0.0" + checksum: 10c0/63b1716eae27947adde49e21b7225a0f75fb2c3d410273ae9de8333c07c7d5fc7a0628ae4c8af6b4b49b4274ed46c2bf118ed69b64f1261c9d8213d76ed1c16c + languageName: node + linkType: hard + +"superagent@npm:^10.3.0": + version: 10.3.0 + resolution: "superagent@npm:10.3.0" + dependencies: + component-emitter: "npm:^1.3.1" + cookiejar: "npm:^2.1.4" + debug: "npm:^4.3.7" + fast-safe-stringify: "npm:^2.1.1" + form-data: "npm:^4.0.5" + formidable: "npm:^3.5.4" + methods: "npm:^1.1.2" + mime: "npm:2.6.0" + qs: "npm:^6.14.1" + checksum: 10c0/7792ec2ba3a877eb1db57ad9149bf0e108688a40af0e75084bdc4bba82604b7962248267159565be4f5ab8aa392f1285a08d2e5a8cb2c3b0a51932499caf6ee6 + languageName: node + linkType: hard + +"supertest@npm:^7.2.2": + version: 7.2.2 + resolution: "supertest@npm:7.2.2" + dependencies: + cookie-signature: "npm:^1.2.2" + methods: "npm:^1.1.2" + superagent: "npm:^10.3.0" + checksum: 10c0/9de987aefbec50c5dfac79ff699bbc23c89cdbfe59ede165309fb3cf00c306117b30c5059fe6f7085c7525aab315ac27c77a1dc6056b993c7e0bb154a56c2b78 + languageName: node + linkType: hard + +"synckit@npm:^0.11.12": + version: 0.11.12 + resolution: "synckit@npm:0.11.12" + dependencies: + "@pkgr/core": "npm:^0.2.9" + checksum: 10c0/cc4d446806688ae0d728ae7bb3f53176d065cf9536647fb85bdd721dcefbd7bf94874df6799ff61580f2b03a392659219b778a9254ad499f9a1f56c34787c235 + languageName: node + linkType: hard + +"tar@npm:^7.5.4": + version: 7.5.9 + resolution: "tar@npm:7.5.9" + dependencies: + "@isaacs/fs-minipass": "npm:^4.0.0" + chownr: "npm:^3.0.0" + minipass: "npm:^7.1.2" + minizlib: "npm:^3.1.0" + yallist: "npm:^5.0.0" + checksum: 10c0/e870beb1b2477135ca2abe86b2d18f7b35d0a4e3a37bbc523d3b8f7adca268dfab543f26528a431d569897f8c53a7cac745cdfbc4411c2f89aeeacc652b81b0a + languageName: node + linkType: hard + +"tinybench@npm:^2.9.0": + version: 2.9.0 + resolution: "tinybench@npm:2.9.0" + checksum: 10c0/c3500b0f60d2eb8db65250afe750b66d51623057ee88720b7f064894a6cb7eb93360ca824a60a31ab16dab30c7b1f06efe0795b352e37914a9d4bad86386a20c + languageName: node + linkType: hard + +"tinyexec@npm:^1.0.2": + version: 1.0.2 + resolution: "tinyexec@npm:1.0.2" + checksum: 10c0/1261a8e34c9b539a9aae3b7f0bb5372045ff28ee1eba035a2a059e532198fe1a182ec61ac60fa0b4a4129f0c4c4b1d2d57355b5cb9aa2d17ac9454ecace502ee + languageName: node + linkType: hard + +"tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.15": + version: 0.2.15 + resolution: "tinyglobby@npm:0.2.15" + dependencies: + fdir: "npm:^6.5.0" + picomatch: "npm:^4.0.3" + checksum: 10c0/869c31490d0d88eedb8305d178d4c75e7463e820df5a9b9d388291daf93e8b1eb5de1dad1c1e139767e4269fe75f3b10d5009b2cc14db96ff98986920a186844 + languageName: node + linkType: hard + +"tinyrainbow@npm:^3.0.3": + version: 3.0.3 + resolution: "tinyrainbow@npm:3.0.3" + checksum: 10c0/1e799d35cd23cabe02e22550985a3051dc88814a979be02dc632a159c393a998628eacfc558e4c746b3006606d54b00bcdea0c39301133956d10a27aa27e988c + languageName: node + linkType: hard + +"toidentifier@npm:~1.0.1": + version: 1.0.1 + resolution: "toidentifier@npm:1.0.1" + checksum: 10c0/93937279934bd66cc3270016dd8d0afec14fb7c94a05c72dc57321f8bd1fa97e5bea6d1f7c89e728d077ca31ea125b78320a616a6c6cd0e6b9cb94cb864381c1 + languageName: node + linkType: hard + +"tslib@npm:^2.4.0": + version: 2.8.1 + resolution: "tslib@npm:2.8.1" + checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62 + languageName: node + linkType: hard + +"tsx@npm:^4.21.0": + version: 4.21.0 + resolution: "tsx@npm:4.21.0" + dependencies: + esbuild: "npm:~0.27.0" + fsevents: "npm:~2.3.3" + get-tsconfig: "npm:^4.7.5" + dependenciesMeta: + fsevents: + optional: true + bin: + tsx: dist/cli.mjs + checksum: 10c0/f5072923cd8459a1f9a26df87823a2ab5754641739d69df2a20b415f61814322b751fa6be85db7c6ec73cf68ba8fac2fd1cfc76bdb0aa86ded984d84d5d2126b + languageName: node + linkType: hard + +"twtxt-lib@npm:^0.9.4": + version: 0.9.4 + resolution: "twtxt-lib@npm:0.9.4" + dependencies: + "@exodus/blakejs": "npm:^1.1.1-exodus.0" + base32.js: "npm:^0.1.0" + dayjs: "npm:^1.11.19" + checksum: 10c0/95c28d47a61f5ff9cef45a9d24109edfb57ceec2386db94cae7ead61d9c9ed142ffc83543e26df78979704cb90325d4cd83f0214ee026aed66a16e56d9395c6f + languageName: node + linkType: hard + +"type-check@npm:^0.4.0, type-check@npm:~0.4.0": + version: 0.4.0 + resolution: "type-check@npm:0.4.0" + dependencies: + prelude-ls: "npm:^1.2.1" + checksum: 10c0/7b3fd0ed43891e2080bf0c5c504b418fbb3e5c7b9708d3d015037ba2e6323a28152ec163bcb65212741fa5d2022e3075ac3c76440dbd344c9035f818e8ecee58 + languageName: node + linkType: hard + +"type-is@npm:^2.0.1": + version: 2.0.1 + resolution: "type-is@npm:2.0.1" + dependencies: + content-type: "npm:^1.0.5" + media-typer: "npm:^1.1.0" + mime-types: "npm:^3.0.0" + checksum: 10c0/7f7ec0a060b16880bdad36824ab37c26019454b67d73e8a465ed5a3587440fbe158bc765f0da68344498235c877e7dbbb1600beccc94628ed05599d667951b99 + languageName: node + linkType: hard + +"typedarray-to-buffer@npm:^3.1.5": + version: 3.1.5 + resolution: "typedarray-to-buffer@npm:3.1.5" + dependencies: + is-typedarray: "npm:^1.0.0" + checksum: 10c0/4ac5b7a93d604edabf3ac58d3a2f7e07487e9f6e98195a080e81dbffdc4127817f470f219d794a843b87052cedef102b53ac9b539855380b8c2172054b7d5027 + languageName: node + linkType: hard + +"typescript@npm:^5.9.3": + version: 5.9.3 + resolution: "typescript@npm:5.9.3" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10c0/6bd7552ce39f97e711db5aa048f6f9995b53f1c52f7d8667c1abdc1700c68a76a308f579cd309ce6b53646deb4e9a1be7c813a93baaf0a28ccd536a30270e1c5 + languageName: node + linkType: hard + +"typescript@patch:typescript@npm%3A^5.9.3#optional!builtin": + version: 5.9.3 + resolution: "typescript@patch:typescript@npm%3A5.9.3#optional!builtin::version=5.9.3&hash=5786d5" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10c0/ad09fdf7a756814dce65bc60c1657b40d44451346858eea230e10f2e95a289d9183b6e32e5c11e95acc0ccc214b4f36289dcad4bf1886b0adb84d711d336a430 + languageName: node + linkType: hard + +"uid-safe@npm:~2.1.5": + version: 2.1.5 + resolution: "uid-safe@npm:2.1.5" + dependencies: + random-bytes: "npm:~1.0.0" + checksum: 10c0/ec96862e859fd12175f3da7fda9d1359a2cf412fd521e10837cbdc6d554774079ce252f366981df9401283841c8924782f6dbee8f82a3a81f805ed8a8584595d + languageName: node + linkType: hard + +"undici-types@npm:~7.18.0": + version: 7.18.2 + resolution: "undici-types@npm:7.18.2" + checksum: 10c0/85a79189113a238959d7a647368e4f7c5559c3a404ebdb8fc4488145ce9426fcd82252a844a302798dfc0e37e6fb178ff481ed03bc4caf634c5757d9ef43521d + languageName: node + linkType: hard + +"unique-filename@npm:^5.0.0": + version: 5.0.0 + resolution: "unique-filename@npm:5.0.0" + dependencies: + unique-slug: "npm:^6.0.0" + checksum: 10c0/afb897e9cf4c2fb622ea716f7c2bb462001928fc5f437972213afdf1cc32101a230c0f1e9d96fc91ee5185eca0f2feb34127145874975f347be52eb91d6ccc2c + languageName: node + linkType: hard + +"unique-slug@npm:^6.0.0": + version: 6.0.0 + resolution: "unique-slug@npm:6.0.0" + dependencies: + imurmurhash: "npm:^0.1.4" + checksum: 10c0/da7ade4cb04eb33ad0499861f82fe95ce9c7c878b7139dc54d140ecfb6a6541c18a5c8dac16188b8b379fe62c0c1f1b710814baac910cde5f4fec06212126c6a + languageName: node + linkType: hard + +"universalify@npm:^0.1.0": + version: 0.1.2 + resolution: "universalify@npm:0.1.2" + checksum: 10c0/e70e0339f6b36f34c9816f6bf9662372bd241714dc77508d231d08386d94f2c4aa1ba1318614f92015f40d45aae1b9075cd30bd490efbe39387b60a76ca3f045 + languageName: node + linkType: hard + +"unpipe@npm:~1.0.0": + version: 1.0.0 + resolution: "unpipe@npm:1.0.0" + checksum: 10c0/193400255bd48968e5c5383730344fbb4fa114cdedfab26e329e50dd2d81b134244bb8a72c6ac1b10ab0281a58b363d06405632c9d49ca9dfd5e90cbd7d0f32c + languageName: node + linkType: hard + +"uri-js@npm:^4.2.2": + version: 4.4.1 + resolution: "uri-js@npm:4.4.1" + dependencies: + punycode: "npm:^2.1.0" + checksum: 10c0/4ef57b45aa820d7ac6496e9208559986c665e49447cb072744c13b66925a362d96dd5a46c4530a6b8e203e5db5fe849369444440cb22ecfc26c679359e5dfa3c + languageName: node + linkType: hard + +"uuid@npm:^13.0.0": + version: 13.0.0 + resolution: "uuid@npm:13.0.0" + bin: + uuid: dist-node/bin/uuid + checksum: 10c0/950e4c18d57fef6c69675344f5700a08af21e26b9eff2bf2180427564297368c538ea11ac9fb2e6528b17fc3966a9fd2c5049361b0b63c7d654f3c550c9b3d67 + languageName: node + linkType: hard + +"vary@npm:^1.1.2": + version: 1.1.2 + resolution: "vary@npm:1.1.2" + checksum: 10c0/f15d588d79f3675135ba783c91a4083dcd290a2a5be9fcb6514220a1634e23df116847b1cc51f66bfb0644cf9353b2abb7815ae499bab06e46dd33c1a6bf1f4f + languageName: node + linkType: hard + +"vite@npm:^6.0.0 || ^7.0.0 || ^8.0.0-0": + version: 8.0.0 + resolution: "vite@npm:8.0.0" + dependencies: + "@oxc-project/runtime": "npm:0.115.0" + fsevents: "npm:~2.3.3" + lightningcss: "npm:^1.32.0" + picomatch: "npm:^4.0.3" + postcss: "npm:^8.5.8" + rolldown: "npm:1.0.0-rc.9" + tinyglobby: "npm:^0.2.15" + peerDependencies: + "@types/node": ^20.19.0 || >=22.12.0 + "@vitejs/devtools": ^0.0.0-alpha.31 + esbuild: ^0.27.0 + jiti: ">=1.21.0" + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: ">=0.54.8" + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + dependenciesMeta: + fsevents: + optional: true + peerDependenciesMeta: + "@types/node": + optional: true + "@vitejs/devtools": + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + bin: + vite: bin/vite.js + checksum: 10c0/2246d3d54788dcd53c39da82da3f934a760756642ba9a575c84c5ef9f310bc47697f7f9fde6721fa566675e93e408736b4ac068008d2ddbd75b0ed99c7fd4c67 + languageName: node + linkType: hard + +"vitest@npm:^4.1.0": + version: 4.1.0 + resolution: "vitest@npm:4.1.0" + dependencies: + "@vitest/expect": "npm:4.1.0" + "@vitest/mocker": "npm:4.1.0" + "@vitest/pretty-format": "npm:4.1.0" + "@vitest/runner": "npm:4.1.0" + "@vitest/snapshot": "npm:4.1.0" + "@vitest/spy": "npm:4.1.0" + "@vitest/utils": "npm:4.1.0" + es-module-lexer: "npm:^2.0.0" + expect-type: "npm:^1.3.0" + magic-string: "npm:^0.30.21" + obug: "npm:^2.1.1" + pathe: "npm:^2.0.3" + picomatch: "npm:^4.0.3" + std-env: "npm:^4.0.0-rc.1" + tinybench: "npm:^2.9.0" + tinyexec: "npm:^1.0.2" + tinyglobby: "npm:^0.2.15" + tinyrainbow: "npm:^3.0.3" + vite: "npm:^6.0.0 || ^7.0.0 || ^8.0.0-0" + why-is-node-running: "npm:^2.3.0" + peerDependencies: + "@edge-runtime/vm": "*" + "@opentelemetry/api": ^1.9.0 + "@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0 + "@vitest/browser-playwright": 4.1.0 + "@vitest/browser-preview": 4.1.0 + "@vitest/browser-webdriverio": 4.1.0 + "@vitest/ui": 4.1.0 + happy-dom: "*" + jsdom: "*" + vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0 + peerDependenciesMeta: + "@edge-runtime/vm": + optional: true + "@opentelemetry/api": + optional: true + "@types/node": + optional: true + "@vitest/browser-playwright": + optional: true + "@vitest/browser-preview": + optional: true + "@vitest/browser-webdriverio": + optional: true + "@vitest/ui": + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vite: + optional: false + bin: + vitest: vitest.mjs + checksum: 10c0/48048e4391e4e8190aa12b1c868bef4ad8d346214631b4506e0dc1f3241ecb8bcb24f296c38a7d98eae712a042375ae209da4b35165db38f9a9bc79a3a9e2a04 + languageName: node + linkType: hard + +"which@npm:^2.0.1": + version: 2.0.2 + resolution: "which@npm:2.0.2" + dependencies: + isexe: "npm:^2.0.0" + bin: + node-which: ./bin/node-which + checksum: 10c0/66522872a768b60c2a65a57e8ad184e5372f5b6a9ca6d5f033d4b0dc98aff63995655a7503b9c0a2598936f532120e81dd8cc155e2e92ed662a2b9377cc4374f + languageName: node + linkType: hard + +"which@npm:^6.0.0": + version: 6.0.1 + resolution: "which@npm:6.0.1" + dependencies: + isexe: "npm:^4.0.0" + bin: + node-which: bin/which.js + checksum: 10c0/7e710e54ea36d2d6183bee2f9caa27a3b47b9baf8dee55a199b736fcf85eab3b9df7556fca3d02b50af7f3dfba5ea3a45644189836df06267df457e354da66d5 + languageName: node + linkType: hard + +"why-is-node-running@npm:^2.3.0": + version: 2.3.0 + resolution: "why-is-node-running@npm:2.3.0" + dependencies: + siginfo: "npm:^2.0.0" + stackback: "npm:0.0.2" + bin: + why-is-node-running: cli.js + checksum: 10c0/1cde0b01b827d2cf4cb11db962f3958b9175d5d9e7ac7361d1a7b0e2dc6069a263e69118bd974c4f6d0a890ef4eedfe34cf3d5167ec14203dbc9a18620537054 + languageName: node + linkType: hard + +"word-wrap@npm:^1.2.5": + version: 1.2.5 + resolution: "word-wrap@npm:1.2.5" + checksum: 10c0/e0e4a1ca27599c92a6ca4c32260e8a92e8a44f4ef6ef93f803f8ed823f486e0889fc0b93be4db59c8d51b3064951d25e43d434e95dc8c960cc3a63d65d00ba20 + languageName: node + linkType: hard + +"wrappy@npm:1": + version: 1.0.2 + resolution: "wrappy@npm:1.0.2" + checksum: 10c0/56fece1a4018c6a6c8e28fbc88c87e0fbf4ea8fd64fc6c63b18f4acc4bd13e0ad2515189786dd2c30d3eec9663d70f4ecf699330002f8ccb547e4a18231fc9f0 + languageName: node + linkType: hard + +"write-file-atomic@npm:3.0.3": + version: 3.0.3 + resolution: "write-file-atomic@npm:3.0.3" + dependencies: + imurmurhash: "npm:^0.1.4" + is-typedarray: "npm:^1.0.0" + signal-exit: "npm:^3.0.2" + typedarray-to-buffer: "npm:^3.1.5" + checksum: 10c0/7fb67affd811c7a1221bed0c905c26e28f0041e138fb19ccf02db57a0ef93ea69220959af3906b920f9b0411d1914474cdd90b93a96e5cd9e8368d9777caac0e + languageName: node + linkType: hard + +"yallist@npm:^4.0.0": + version: 4.0.0 + resolution: "yallist@npm:4.0.0" + checksum: 10c0/2286b5e8dbfe22204ab66e2ef5cc9bbb1e55dfc873bbe0d568aa943eb255d131890dfd5bf243637273d31119b870f49c18fcde2c6ffbb7a7a092b870dc90625a + languageName: node + linkType: hard + +"yallist@npm:^5.0.0": + version: 5.0.0 + resolution: "yallist@npm:5.0.0" + checksum: 10c0/a499c81ce6d4a1d260d4ea0f6d49ab4da09681e32c3f0472dee16667ed69d01dae63a3b81745a24bd78476ec4fcf856114cb4896ace738e01da34b2c42235416 + languageName: node + linkType: hard + +"yocto-queue@npm:^0.1.0": + version: 0.1.0 + resolution: "yocto-queue@npm:0.1.0" + checksum: 10c0/dceb44c28578b31641e13695d200d34ec4ab3966a5729814d5445b194933c096b7ced71494ce53a0e8820685d1d010df8b2422e5bf2cdea7e469d97ffbea306f + languageName: node + linkType: hard + +"zod@npm:^4.3.6": + version: 4.3.6 + resolution: "zod@npm:4.3.6" + checksum: 10c0/860d25a81ab41d33aa25f8d0d07b091a04acb426e605f396227a796e9e800c44723ed96d0f53a512b57be3d1520f45bf69c0cb3b378a232a00787a2609625307 + languageName: node + linkType: hard