diff --git a/favicon-shell.nix b/favicon-shell.nix new file mode 100644 index 0000000..d9913e9 --- /dev/null +++ b/favicon-shell.nix @@ -0,0 +1,92 @@ +{ pkgs ? import {} }: + +pkgs.buildFHSEnv { + name = "puppeteer-env"; + targetPkgs = pkgs: with pkgs; [ + nodejs_latest + nodePackages.pnpm + nodePackages.pm2 + + # Core libraries that Chrome needs + expat + nss + nspr + alsa-lib + atk + cups + dbus + glib + gtk3 + pango + cairo + libxkbcommon + + # X11 libraries + xorg.libX11 + xorg.libXcomposite + xorg.libXdamage + xorg.libXrandr + xorg.libxshmfence + xorg.libXext + xorg.libXfixes + xorg.libXrender + xorg.libXtst + xorg.libXScrnSaver + + # Graphics and system libraries + mesa + udev + libdrm + systemd + + # Additional libraries Chrome might need + fontconfig + freetype + zlib + libpng + libjpeg + + # Utilities + which + chromium # This ensures all Chrome dependencies are available + ]; + + runScript = "bash"; + + extraBuildCommands = '' + export NODE_ENV=development + export LD_LIBRARY_PATH=${pkgs.lib.makeLibraryPath (with pkgs; [ + expat + nss + nspr + alsa-lib + atk + cups + dbus + glib + gtk3 + pango + cairo + libxkbcommon + xorg.libX11 + xorg.libXcomposite + xorg.libXdamage + xorg.libXrandr + xorg.libxshmfence + xorg.libXext + xorg.libXfixes + xorg.libXrender + xorg.libXtst + xorg.libXScrnSaver + mesa + udev + libdrm + systemd + fontconfig + freetype + zlib + libpng + libjpeg + ])}:$LD_LIBRARY_PATH + ''; +} diff --git a/package-lock.json b/package-lock.json index 47d9f99..a1bebb0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,8 +30,12 @@ "nodemailer": "^7.0.3", "nodemon": "^3.1.10", "path": "^0.12.7", + "puppeteer": "^24.12.1", "rss": "^1.2.2", + "serve-favicon": "^2.5.1", + "sharp": "^0.34.3", "sqlite3": "^5.1.7", + "to-ico": "^1.1.5", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0", "xss": "^1.0.15", @@ -44,6 +48,29 @@ "postcss-import": "^16.1.1" } }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@colors/colors": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", @@ -64,6 +91,23 @@ "kuler": "^2.0.0" } }, + "node_modules/@emnapi/runtime": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.4.tgz", + "integrity": "sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, "node_modules/@faker-js/faker": { "version": "9.8.0", "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.8.0.tgz", @@ -88,6 +132,424 @@ "license": "MIT", "optional": true }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz", + "integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz", + "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz", + "integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz", + "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz", + "integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz", + "integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz", + "integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz", + "integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz", + "integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz", + "integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz", + "integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz", + "integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz", + "integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz", + "integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz", + "integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz", + "integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz", + "integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz", + "integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz", + "integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.4.4" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz", + "integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz", + "integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz", + "integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -375,6 +837,80 @@ "debug": "^4.3.1" } }, + "node_modules/@puppeteer/browsers": { + "version": "2.10.5", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.5.tgz", + "integrity": "sha512-eifa0o+i8dERnngJwKrfp3dEq7ia5XFyoqB17S4gK8GhsQE4/P8nxOfQSE0zQHxzzLo/cmF+7+ywEQ7wK7Fb+w==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.1", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.2", + "tar-fs": "^3.0.8", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@puppeteer/browsers/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/@puppeteer/browsers/node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@puppeteer/browsers/node_modules/tar-fs": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.0.tgz", + "integrity": "sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/@puppeteer/browsers/node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, "node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -389,15 +925,34 @@ "version": "0.23.0", "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "24.0.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.13.tgz", + "integrity": "sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "undici-types": "~7.8.0" + } + }, "node_modules/@types/triple-beam": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", "license": "MIT" }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/abbrev": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", @@ -424,7 +979,6 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 14" @@ -457,6 +1011,22 @@ "node": ">=8" } }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/amp": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/amp/-/amp-0.3.1.tgz", @@ -562,11 +1132,37 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, "node_modules/ast-types": { "version": "0.13.4", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", - "dev": true, "license": "MIT", "dependencies": { "tslib": "^2.0.1" @@ -579,7 +1175,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, "node_modules/async": { @@ -588,12 +1183,111 @@ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "license": "MIT" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "license": "MIT" + }, + "node_modules/b4a": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "license": "Apache-2.0" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/bare-events": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.6.0.tgz", + "integrity": "sha512-EKZ5BTXYExaNqi3I3f9RtEsaI/xBSGjE0XZCZilPzFAV/goswFHuPd9jEZlPIZ/iNZJwDSao9qRiScySz7MbQg==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/bare-fs": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.6.tgz", + "integrity": "sha512-25RsLF33BqooOEFNdMcEhMpJy8EoR88zSMrnOQOaM3USnOK2VmaJ1uaQEwPA6AQjrv1lXChScosN6CzbwbO9OQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz", + "integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz", + "integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "streamx": "^2.21.0" + }, + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -636,12 +1330,20 @@ "version": "5.0.5", "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", - "dev": true, "license": "MIT", "engines": { "node": ">=10.0.0" } }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, "node_modules/better-sqlite3": { "version": "12.2.0", "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.2.0.tgz", @@ -656,6 +1358,15 @@ "node": "20.x || 22.x || 23.x || 24.x" } }, + "node_modules/bignumber.js": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-2.4.0.tgz", + "integrity": "sha512-uw4ra6Cv483Op/ebM0GBKKfxZlSmn6NgFRby5L3yGTlunLj53KQgndDlqy2WVFOwgvurocApYkSud0aO+mvrpQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -701,6 +1412,12 @@ "node": ">= 0.8.0" } }, + "node_modules/bmp-js": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.0.1.tgz", + "integrity": "sha512-OS74Rlt0Aynu2mTPmY9RZOUOXlqWecFIILFXr70vv16/xCZnFxvri9IKkF1IGxQ8r9dOE62qGNpKxXx8Lko8bg==", + "license": "MIT" + }, "node_modules/bodec": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/bodec/-/bodec-0.1.0.tgz", @@ -773,6 +1490,46 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "license": "MIT", + "dependencies": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "node_modules/buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "license": "MIT" + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-0.0.1.tgz", + "integrity": "sha512-RgSV6InVQ9ODPdLWJ5UAqBqJBOg370Nz6ZQtRzpt6nUjc8v0St97uJ4PYC6NztqIScrAXafKM3mZPMygSe1ggA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", + "license": "MIT" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -920,6 +1677,30 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "license": "Apache-2.0" + }, + "node_modules/centra": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/centra/-/centra-2.7.0.tgz", + "integrity": "sha512-PbFMgMSrmgx6uxCdm57RUos9Tc3fclMvhLSATYN39XsDV29B89zZ3KA89jmY0vwSGazyU+uerqwa6t+KaodPcg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6" + } + }, "node_modules/chalk": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", @@ -1013,6 +1794,19 @@ "node": ">=10" } }, + "node_modules/chromium-bidi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-5.1.0.tgz", + "integrity": "sha512-9MSRhWRVoRPDG0TgzkHrshFSJJNZzfY5UFqUMuksg7zL1yoZIZ3jLB0YAgHclbiAxPI86pBnwDX1tbzoiV8aFw==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -1035,6 +1829,93 @@ "node": ">=8.10.0" } }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/color": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", @@ -1108,6 +1989,18 @@ "text-hex": "1.0.x" } }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", @@ -1252,6 +2145,56 @@ "node": ">=6.6.0" } }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cosmiconfig/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/cosmiconfig/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/croner": { "version": "4.1.97", "resolved": "https://registry.npmjs.org/croner/-/croner-4.1.97.tgz", @@ -1387,11 +2330,22 @@ "dev": true, "license": "MIT" }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/data-uri-to-buffer": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 14" @@ -1449,7 +2403,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", - "dev": true, "license": "MIT", "dependencies": { "ast-types": "^0.13.4", @@ -1460,6 +2413,15 @@ "node": ">= 14" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -1485,6 +2447,17 @@ "node": ">=8" } }, + "node_modules/devtools-protocol": { + "version": "0.0.1464554", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1464554.tgz", + "integrity": "sha512-CAoP3lYfwAGQTaAXYvA6JZR0fjGUb7qec1qf4mToyoH2TZgUFeIqYcjh6f9jNuhHfuZiEdH+PONHYrLhRQX6aw==", + "license": "BSD-3-Clause" + }, + "node_modules/dom-walk": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", + "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" + }, "node_modules/dotenv": { "version": "16.5.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", @@ -1517,6 +2490,22 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "license": "MIT" }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "license": "MIT", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/ecc-jsbn/node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "license": "MIT" + }, "node_modules/editorconfig": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz", @@ -1614,7 +2603,6 @@ "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", "license": "MIT", - "optional": true, "engines": { "node": ">=6" } @@ -1626,6 +2614,21 @@ "license": "MIT", "optional": true }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/error-ex/node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -1656,6 +2659,21 @@ "node": ">= 0.4" } }, + "node_modules/es6-promise": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", + "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -1679,7 +2697,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "esprima": "^4.0.1", @@ -1714,7 +2731,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -1724,7 +2740,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" @@ -1746,6 +2761,11 @@ "dev": true, "license": "MIT" }, + "node_modules/exif-parser": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz", + "integrity": "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==" + }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -1826,6 +2846,12 @@ "express": "^4.11 || 5 || ^5.0.0-beta.1" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/extend-shallow": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", @@ -1838,6 +2864,26 @@ "node": ">=0.10.0" } }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, "node_modules/extrareqp2": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/extrareqp2/-/extrareqp2-1.0.0.tgz", @@ -1848,6 +2894,27 @@ "follow-redirects": "^1.14.0" } }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, "node_modules/fast-json-patch": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz", @@ -1855,6 +2922,12 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, "node_modules/fclone": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/fclone/-/fclone-1.0.11.tgz", @@ -1862,6 +2935,15 @@ "dev": true, "license": "MIT" }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, "node_modules/fecha": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", @@ -1877,6 +2959,15 @@ "moment": "^2.29.1" } }, + "node_modules/file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -1922,7 +3013,6 @@ "version": "1.15.9", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", - "dev": true, "funding": [ { "type": "individual", @@ -1961,6 +3051,50 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -2112,6 +3246,15 @@ "node": ">=8" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -2149,11 +3292,25 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-uri": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.4.tgz", "integrity": "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==", - "dev": true, "license": "MIT", "dependencies": { "basic-ftp": "^5.0.2", @@ -2164,6 +3321,15 @@ "node": ">= 14" } }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, "node_modules/git-node-fs": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/git-node-fs/-/git-node-fs-1.0.0.tgz", @@ -2219,6 +3385,16 @@ "node": ">= 6" } }, + "node_modules/global": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", + "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", + "license": "MIT", + "dependencies": { + "min-document": "^2.19.0", + "process": "^0.11.10" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -2273,6 +3449,29 @@ "uglify-js": "^3.1.4" } }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "license": "MIT", + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -2440,7 +3639,6 @@ "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, "license": "MIT", "dependencies": { "agent-base": "^7.1.0", @@ -2450,11 +3648,25 @@ "node": ">= 14" } }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, "license": "MIT", "dependencies": { "agent-base": "^7.1.2", @@ -2512,6 +3724,34 @@ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", "license": "ISC" }, + "node_modules/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "license": "MIT", + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -2567,7 +3807,6 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", - "devOptional": true, "license": "MIT", "dependencies": { "jsbn": "1.1.0", @@ -2581,9 +3820,17 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "devOptional": true, "license": "BSD-3-Clause" }, + "node_modules/ip-regex": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-1.0.3.tgz", + "integrity": "sha512-HjpCHTuxbR/6jWJroc/VN+npo5j0T4Vv2TAI5qdEHQx7hsL767MeccGFSsLtF694EiZKTSEqgoeU6DtGFCcuqQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -2654,6 +3901,12 @@ "node": ">=8" } }, + "node_modules/is-function": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz", + "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==", + "license": "MIT" + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -2700,12 +3953,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "license": "MIT" + }, "node_modules/jackspeak": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.0.tgz", @@ -2721,6 +3986,67 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/jimp": { + "version": "0.2.28", + "resolved": "https://registry.npmjs.org/jimp/-/jimp-0.2.28.tgz", + "integrity": "sha512-9HT7DA279xkTlry2oG30s6AtOUglNiY2UdyYpj0yNI4/NBv8PmdNC0gcldgMU4HqvbUlrM3+v+6GaHnTkH23JQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^2.1.0", + "bmp-js": "0.0.3", + "es6-promise": "^3.0.2", + "exif-parser": "^0.1.9", + "file-type": "^3.1.0", + "jpeg-js": "^0.2.0", + "load-bmfont": "^1.2.3", + "mime": "^1.3.4", + "mkdirp": "0.5.1", + "pixelmatch": "^4.0.0", + "pngjs": "^3.0.0", + "read-chunk": "^1.0.1", + "request": "^2.65.0", + "stream-to-buffer": "^0.1.0", + "tinycolor2": "^1.1.2", + "url-regex": "^3.0.0" + } + }, + "node_modules/jimp/node_modules/bmp-js": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.0.3.tgz", + "integrity": "sha512-epsm3Z92j5xwek9p97pVw3KbsNc0F4QnbYh+N93SpbJYuHFQQ/UAh6K+bKFGyLePH3Hudtl/Sa95Quqp0gX8IQ==", + "license": "MIT" + }, + "node_modules/jimp/node_modules/jpeg-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.2.0.tgz", + "integrity": "sha512-Ni9PffhJtYtdD7VwxH6V2MnievekGfUefosGCHadog0/jAevRu6HPjYeMHbUemn0IPE8d4wGa8UsOGsX+iKy2g==", + "license": "BSD-3-Clause" + }, + "node_modules/jimp/node_modules/minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha512-miQKw5Hv4NS1Psg2517mV4e4dYNaO3++hjAvLOAzKqZ61rH8NS1SK+vbfBWZ5PY/Me/bEWhUwqMghEW5Fb9T7Q==", + "license": "MIT" + }, + "node_modules/jimp/node_modules/mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha512-SknJC52obPfGQPnjIkXbmA6+5H15E+fR+E4iR2oQ3zzCLbd7/ONua69R/Gw7AgkTLsRG+r5fzksYwWe1AgTyWA==", + "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)", + "license": "MIT", + "dependencies": { + "minimist": "0.0.8" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/jpeg-js": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.1.2.tgz", + "integrity": "sha512-CiRVjMKBUp6VYtGwzRjrdnro41yMwKGOWdP9ATXqmixdz2n7KHNwdqthTYSSbOKj/Ha79Gct1sA8ZQpse55TYA==", + "license": "BSD-3-Clause" + }, "node_modules/js-beautify": { "version": "1.15.4", "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz", @@ -2836,6 +4162,12 @@ "pako": "^0.2.5" } }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, "node_modules/js-yaml": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", @@ -2853,16 +4185,46 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", - "devOptional": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "license": "MIT" }, "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true, - "license": "ISC", - "optional": true + "license": "ISC" + }, + "node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "license": "MIT", + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } }, "node_modules/kind-of": { "version": "6.0.3", @@ -2879,6 +4241,28 @@ "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", "license": "MIT" }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/load-bmfont": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/load-bmfont/-/load-bmfont-1.4.2.tgz", + "integrity": "sha512-qElWkmjW9Oq1F9EI5Gt7aD9zcdHb9spJCW1L/dmPf7KzCCEJxq8nhHz5eCgI9aMf7vrG/wyaCqdsI+Iy9ZTlog==", + "license": "MIT", + "dependencies": { + "buffer-equal": "0.0.1", + "mime": "^1.3.4", + "parse-bmfont-ascii": "^1.0.3", + "parse-bmfont-binary": "^1.0.5", + "parse-bmfont-xml": "^1.1.4", + "phin": "^3.7.1", + "xhr": "^2.0.1", + "xtend": "^4.0.0" + } + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -3074,6 +4458,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -3107,6 +4503,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/min-document": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", + "integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==", + "dependencies": { + "dom-walk": "^0.1.0" + } + }, "node_modules/minimatch": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", @@ -3300,6 +4704,12 @@ "node": ">=8" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -3475,7 +4885,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4.0" @@ -3712,6 +5121,24 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-hash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", @@ -3792,7 +5219,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", - "dev": true, "license": "MIT", "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", @@ -3812,7 +5238,6 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", - "dev": true, "license": "MIT", "dependencies": { "degenerator": "^5.0.0", @@ -3835,6 +5260,76 @@ "dev": true, "license": "MIT" }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-bmfont-ascii": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-ascii/-/parse-bmfont-ascii-1.0.6.tgz", + "integrity": "sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA==", + "license": "MIT" + }, + "node_modules/parse-bmfont-binary": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-binary/-/parse-bmfont-binary-1.0.6.tgz", + "integrity": "sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA==", + "license": "MIT" + }, + "node_modules/parse-bmfont-xml": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-xml/-/parse-bmfont-xml-1.1.6.tgz", + "integrity": "sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA==", + "license": "MIT", + "dependencies": { + "xml-parse-from-string": "^1.0.0", + "xml2js": "^0.5.0" + } + }, + "node_modules/parse-headers": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.6.tgz", + "integrity": "sha512-Tz11t3uKztEW5FEVZnj1ox8GKblWn+PvHY9TmJV5Mll2uHEwRdR/5Li1OlXoECjLYkApdhWy44ocONwXLiKO5A==", + "license": "MIT" + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-png": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/parse-png/-/parse-png-1.1.2.tgz", + "integrity": "sha512-Ge6gDV9T5zhkWHmjvnNiyhPTCIoY7W+FC7qWPtuL2lIGZAFxxqTRG/ouEXsH9qkw+HzYiPEU/tFcxOCEDTP1Yw==", + "license": "MIT", + "dependencies": { + "pngjs": "^3.2.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -3904,11 +5399,34 @@ "node": ">=16" } }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT" + }, + "node_modules/phin": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/phin/-/phin-3.7.1.tgz", + "integrity": "sha512-GEazpTWwTZaEQ9RhL7Nyz0WwqilbqgLahDM3D0hxWwmVDI52nXEybHqiN6/elwpkJBhcuj+WbBu+QfT0uhPGfQ==", + "license": "MIT", + "dependencies": { + "centra": "^2.7.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -3946,6 +5464,39 @@ "node": ">=0.10.0" } }, + "node_modules/pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", + "license": "MIT", + "dependencies": { + "pinkie": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pixelmatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-4.0.2.tgz", + "integrity": "sha512-J8B6xqiO37sU/gkcMglv6h5Jbd9xNER7aHzpfRdNmV4IbQBzBpe4l9XmbG+xPF/znacgu2jfEw+wHffaq/YkXA==", + "license": "ISC", + "dependencies": { + "pngjs": "^3.0.0" + }, + "bin": { + "pixelmatch": "bin/pixelmatch" + } + }, "node_modules/pm2": { "version": "6.0.6", "resolved": "https://registry.npmjs.org/pm2/-/pm2-6.0.6.tgz", @@ -4112,6 +5663,15 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/pngjs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", + "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -4200,6 +5760,15 @@ "node": ">= 0.6.0" } }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -4284,9 +5853,20 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true, "license": "MIT" }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", @@ -4303,6 +5883,74 @@ "once": "^1.3.1" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/puppeteer": { + "version": "24.12.1", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.12.1.tgz", + "integrity": "sha512-+vvwl+Xo4z5uXLLHG+XW8uXnUXQ62oY6KU6bEFZJvHWLutbmv5dw9A/jcMQ0fqpQdLydHmK0Uy7/9Ilj8ufwSQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.10.5", + "chromium-bidi": "5.1.0", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1464554", + "puppeteer-core": "24.12.1", + "typed-query-selector": "^2.12.0" + }, + "bin": { + "puppeteer": "lib/cjs/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core": { + "version": "24.12.1", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.12.1.tgz", + "integrity": "sha512-8odp6d3ERKBa3BAVaYWXn95UxQv3sxvP1reD+xZamaX6ed8nCykhwlOiHSaHR9t/MtmIB+rJmNencI6Zy4Gxvg==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.10.5", + "chromium-bidi": "5.1.0", + "debug": "^4.4.1", + "devtools-protocol": "0.0.1464554", + "typed-query-selector": "^2.12.0", + "ws": "^8.18.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", @@ -4389,6 +6037,15 @@ "pify": "^2.3.0" } }, + "node_modules/read-chunk": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-chunk/-/read-chunk-1.0.1.tgz", + "integrity": "sha512-5NLTTdX45dKFtG8CX5pKmvS9V5u9wBE+gkklN7xhDuhq3pA2I4O7ALfKxosCMcLHOhkxj6GNacZhfXtp5nlCdg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -4415,6 +6072,77 @@ "node": ">=8.10.0" } }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "license": "Apache-2.0", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/request/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/request/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/request/node_modules/qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-in-the-middle": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-5.2.0.tgz", @@ -4430,6 +6158,36 @@ "node": ">=6" } }, + "node_modules/resize-img": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/resize-img/-/resize-img-1.1.2.tgz", + "integrity": "sha512-/4nKUmuNPuM6gYTWad136ica81baOVjpesgv8FGaIvP0KWcbCWahOWBKaM4tFoM+aVcSA+qQDg28pcnIzFRpJw==", + "license": "MIT", + "dependencies": { + "bmp-js": "0.0.1", + "file-type": "^3.8.0", + "get-stream": "^2.0.0", + "jimp": "^0.2.21", + "jpeg-js": "^0.1.1", + "parse-png": "^1.1.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/resize-img/node_modules/get-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", + "integrity": "sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==", + "license": "MIT", + "dependencies": { + "object-assign": "^4.0.1", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -4451,6 +6209,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -4637,7 +6404,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", - "dev": true, "license": "ISC" }, "node_modules/section-matter": { @@ -4687,6 +6453,31 @@ "node": ">= 18" } }, + "node_modules/serve-favicon": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/serve-favicon/-/serve-favicon-2.5.1.tgz", + "integrity": "sha512-JndLBslCLA/ebr7rS3d+/EKkzTsTi1jI2T9l+vHfAaGJ7A7NhtDpSZ0lx81HCNWnnE0yHncG+SSnVf9IMxOwXQ==", + "license": "MIT", + "dependencies": { + "etag": "~1.8.1", + "fresh": "~0.5.2", + "ms": "~2.1.3", + "parseurl": "~1.3.2", + "safe-buffer": "~5.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-favicon/node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/serve-static": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", @@ -4715,6 +6506,61 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/sharp": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz", + "integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.4", + "semver": "^7.7.2" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.3", + "@img/sharp-darwin-x64": "0.34.3", + "@img/sharp-libvips-darwin-arm64": "1.2.0", + "@img/sharp-libvips-darwin-x64": "1.2.0", + "@img/sharp-libvips-linux-arm": "1.2.0", + "@img/sharp-libvips-linux-arm64": "1.2.0", + "@img/sharp-libvips-linux-ppc64": "1.2.0", + "@img/sharp-libvips-linux-s390x": "1.2.0", + "@img/sharp-libvips-linux-x64": "1.2.0", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", + "@img/sharp-libvips-linuxmusl-x64": "1.2.0", + "@img/sharp-linux-arm": "0.34.3", + "@img/sharp-linux-arm64": "0.34.3", + "@img/sharp-linux-ppc64": "0.34.3", + "@img/sharp-linux-s390x": "0.34.3", + "@img/sharp-linux-x64": "0.34.3", + "@img/sharp-linuxmusl-arm64": "0.34.3", + "@img/sharp-linuxmusl-x64": "0.34.3", + "@img/sharp-wasm32": "0.34.3", + "@img/sharp-win32-arm64": "0.34.3", + "@img/sharp-win32-ia32": "0.34.3", + "@img/sharp-win32-x64": "0.34.3" + } + }, + "node_modules/sharp/node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -4897,7 +6743,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "devOptional": true, "license": "MIT", "engines": { "node": ">= 6.0.0", @@ -4908,7 +6753,6 @@ "version": "2.8.4", "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==", - "devOptional": true, "license": "MIT", "dependencies": { "ip-address": "^9.0.5", @@ -4923,7 +6767,6 @@ "version": "8.0.5", "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "dev": true, "license": "MIT", "dependencies": { "agent-base": "^7.1.2", @@ -4994,6 +6837,37 @@ } } }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "license": "MIT", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sshpk/node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "license": "MIT" + }, "node_modules/ssri": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", @@ -5038,6 +6912,40 @@ "node": ">= 0.8" } }, + "node_modules/stream-to": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stream-to/-/stream-to-0.2.2.tgz", + "integrity": "sha512-Kg1BSDTwgGiVMtTCJNlo7kk/xzL33ZuZveEBRt6rXw+f1WLK/8kmz2NVCT/Qnv0JkV85JOHcLhD82mnXsR3kPw==", + "license": "MIT", + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/stream-to-buffer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/stream-to-buffer/-/stream-to-buffer-0.1.0.tgz", + "integrity": "sha512-Da4WoKaZyu3nf+bIdIifh7IPkFjARBnBK+pYqn0EUJqksjV9afojjaCCHUemH30Jmu7T2qcKvlZm2ykN38uzaw==", + "license": "MIT", + "dependencies": { + "stream-to": "~0.2.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamx": { + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz", + "integrity": "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==", + "license": "MIT", + "dependencies": { + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -5274,12 +7182,43 @@ "node": ">=8" } }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, "node_modules/text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", "license": "MIT" }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "license": "MIT" + }, + "node_modules/to-ico": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/to-ico/-/to-ico-1.1.5.tgz", + "integrity": "sha512-5kIh7m7bkIlqIESEZkL8gAMMzucXKfPe3hX2FoDY5HEAfD9OJU+Qh9b6Enp74w0qRcxVT5ejss66PHKqc3AVkg==", + "license": "MIT", + "dependencies": { + "arrify": "^1.0.1", + "buffer-alloc": "^1.1.0", + "image-size": "^0.5.0", + "parse-png": "^1.0.0", + "resize-img": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5310,6 +7249,19 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -5372,6 +7324,12 @@ "node": ">= 0.8.0" } }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "license": "Unlicense" + }, "node_modules/tx2": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tx2/-/tx2-1.0.5.tgz", @@ -5397,6 +7355,12 @@ "node": ">= 0.6" } }, + "node_modules/typed-query-selector": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", + "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", + "license": "MIT" + }, "node_modules/uglify-js": { "version": "3.19.3", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", @@ -5428,6 +7392,13 @@ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", "license": "MIT" }, + "node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "license": "MIT", + "optional": true + }, "node_modules/unique-filename": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", @@ -5457,6 +7428,27 @@ "node": ">= 0.8" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-regex": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/url-regex/-/url-regex-3.2.0.tgz", + "integrity": "sha512-dQ9cJzMou5OKr6ZzfvwJkCq3rC72PNXhqz0v3EIhF4a3Np+ujr100AhUx2cKx5ei3iymoJpJrPB3sVSEMdqAeg==", + "license": "MIT", + "dependencies": { + "ip-regex": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/util": { "version": "0.10.4", "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", @@ -5476,6 +7468,16 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" }, + "node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "license": "MIT", + "bin": { + "uuid": "bin/uuid" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -5485,6 +7487,20 @@ "node": ">= 0.8" } }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, "node_modules/vizion": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/vizion/-/vizion-2.2.1.tgz", @@ -5785,12 +7801,52 @@ } } }, + "node_modules/xhr": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/xhr/-/xhr-2.6.0.tgz", + "integrity": "sha512-/eCGLb5rxjx5e3mF1A7s+pLlR6CGyqWN91fv1JgER5mVWg1MZmlhBvy9kjcsOdRk8RrIujotWyJamfyrp+WIcA==", + "license": "MIT", + "dependencies": { + "global": "~4.4.0", + "is-function": "^1.0.1", + "parse-headers": "^2.0.0", + "xtend": "^4.0.0" + } + }, "node_modules/xml": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==", "license": "MIT" }, + "node_modules/xml-parse-from-string": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz", + "integrity": "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g==", + "license": "MIT" + }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, "node_modules/xss": { "version": "1.0.15", "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.15.tgz", @@ -5828,11 +7884,116 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "license": "MIT" }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 173746e..5a94c8c 100644 --- a/package.json +++ b/package.json @@ -39,8 +39,12 @@ "nodemailer": "^7.0.3", "nodemon": "^3.1.10", "path": "^0.12.7", + "puppeteer": "^24.12.1", "rss": "^1.2.2", + "serve-favicon": "^2.5.1", + "sharp": "^0.34.3", "sqlite3": "^5.1.7", + "to-ico": "^1.1.5", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0", "xss": "^1.0.15", diff --git a/public/css/redirect.css b/public/css/redirect.css new file mode 100644 index 0000000..08186fd --- /dev/null +++ b/public/css/redirect.css @@ -0,0 +1,75 @@ +.redirect-container { + display: flex; + justify-content: center; + align-items: center; + padding: 2rem; +} + +.redirect-content { + text-align: center; + background: #ffffff; + padding: 3rem 2rem; + border-radius: 6px; + box-shadow: 0 2px 6px rgba(0,0,0,0.05); + max-width: 500px; + width: 100%; +} + +.spinner { + width: 40px; + height: 40px; + border: 4px solid #e9ecef; + border-top: 4px solid #2c3e50; + border-radius: 50%; + animation: spin 1s linear infinite; + margin: 0 auto 1.5rem; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +h1 { + font-size: 1.5rem; + font-weight: 600; + margin-bottom: 1rem; + color: #2c3e50; +} + +p { + font-size: 1rem; + color: #444; + margin-bottom: 1rem; +} + +.redirect-link { + color: #2c3e50; + text-decoration: none; + font-weight: 600; + padding: 0.5rem 1rem; + border: 2px solid #2c3e50; + border-radius: 4px; + display: inline-block; + transition: all 0.2s ease-in-out; +} + +.redirect-link:hover { + background-color: #2c3e50; + color: #ffffff; + text-decoration: none; +} + +@media (max-width: 600px) { + .redirect-content { + padding: 2rem 1rem; + } + + h1 { + font-size: 1.3rem; + } + + p { + font-size: 0.9rem; + } +} diff --git a/public/css/styles.css b/public/css/styles.css index 727dda4..d57e4fe 100644 --- a/public/css/styles.css +++ b/public/css/styles.css @@ -12,6 +12,23 @@ margin: 0 auto; padding: 1rem 1.5rem; } +.pattern-dots { + position: relative; + background-image: radial-gradient(circle, rgba(0,0,0,0.07) 1px, transparent 1px); + background-size: 15px 15px; + z-index: 0; +} +.pattern-dots::after { + content: ""; + pointer-events: none; + position: absolute; + inset: 0; + background: linear-gradient(to bottom, rgba(255,255,255,0) 0%, rgba(255,255,255,1) 90%); +} +.pattern-dots > * { + position: relative; + z-index: 1; +} footer { background-color: #34495e; color: #ecf0f1; @@ -56,10 +73,8 @@ margin-top: 0; margin-bottom: 0; display: flex; - gap: 1.5rem; /* Adjust this value to control spacing between links */ - /* If you want them centered within the nav */ + gap: 1.5rem; justify-content: center; - /* Allow items to wrap to the next line on smaller screens */ flex-wrap: wrap; } footer nav p { @@ -100,26 +115,52 @@ background-color: #2c3e50; color: #ecf0f1; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); + position:relative; + z-index: 10; } -header#site-header .container { +/* header#site-header .container { display: flex; flex-direction: column; align-items: flex-start; gap: 0.5rem; padding: 1rem 0; +} */ +header#site-header .container { + max-width: 800px; + margin: 0 auto; + padding: 1rem 1.5rem; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + position: relative; } +/* Logo section - positioned above nav */ header#site-header .logo { + display: flex; + align-items: center; font-size: 1.8rem; font-weight: bolder; position: relative; + text-decoration: none; + color: inherit; + gap: 1rem; + margin-left: 10px; + padding-left: 64px; /* Space for the icon */ } -header#site-header .logo a { +header#site-header .site-title { color: inherit; text-decoration: none; position: relative; display: inline-block; } -header#site-header .logo a::after { +header#site-header .site-title, a { + color: inherit; + text-decoration: none; + position: relative; + display: inline-block; +} +header#site-header .site-title a::after { content: ""; position: absolute; left: 0; @@ -131,15 +172,33 @@ transform-origin: left; transition: transform 0.3s ease; } -header#site-header .logo a:hover::after { +header#site-header .site-title a:hover::after { transform: scaleX(1); } -.layout { - display: flex; - max-width: 1200px; - margin: 2rem auto; - padding: 0 1.5rem; - gap: 2rem; +.pattern-lambda { + background-image: + repeating-linear-gradient( + 45deg, + transparent, + transparent 50px, + rgba(236, 240, 241, 0.08) 50px, + rgba(236, 240, 241, 0.08) 52px + ); + position: relative; +} +.pattern-lambda::before { + content: ' '; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + font-size: 2rem; + color: rgba(236, 240, 241, 0.05); + letter-spacing: 2rem; + line-height: 3rem; + overflow: hidden; + pointer-events: none; } /* Main content */ main.container { @@ -174,6 +233,8 @@ header#site-header nav.site-nav { display: flex; gap: 1rem; + position: relative; + z-index: 10; } header#site-header nav.site-nav a, nav.site-nav .dropdown-content form button { @@ -208,17 +269,18 @@ nav.site-nav .dropdown { position: relative; display: block; + z-index: 11; } nav.site-nav .dropbtn { display: inline-block; cursor: pointer; } nav.site-nav .dropdown-content { + z-index: 11; display: none; position: absolute; background-color: white; min-width: 160px; - z-index: 1; box-shadow: 0px 8px 16px rgba(0,0,0,0.2); } nav.site-nav .dropdown-content a { @@ -246,6 +308,25 @@ nav.site-nav .dropdown-content form { margin: 0; } +/* Navigation row with consistent left alignment */ +.nav-row { + display: flex; + align-items: center; + gap: 1rem; + width: 100%; + padding-left: 64px; /* Same as icon width + gap to align with site title */ +} +/* Logo icon that acts as home link - spans from site title to nav */ +.nav-logo { + text-decoration: none; + color: inherit; + display: flex; + align-items: flex-end; + position: absolute; + left: 1rem; + top: 1rem; + bottom: 1rem; +} /* Reset */ * { margin: 0; @@ -487,6 +568,12 @@ .sidebar { width: 100%; } + header#site-header .container { + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + } + header#site-header nav.site-nav { flex-direction: column; gap: 0; @@ -508,6 +595,22 @@ header#site-header #siteNav.hide a { display: none; } + nav.site-nav { + flex-direction: column; + gap: 0; + width: 100%; + } + .nav-row { + flex-direction: column; + align-items: flex-start; + width: 100%; + padding-left: 0; + } + .nav-logo { + position: relative; + left: 0; + margin-bottom: 0.5rem; + } nav.site-nav ul { display: block; justify-content: center; @@ -541,3 +644,157 @@ position: static; } } +.icon { + width: 64px; + height: 64px; + display: flex; + justify-content: center; + align-items: center; + font-size: 36px; + user-select: none; +} +.deep-dive-enhanced { + background: linear-gradient(135deg, #0A192F 0%, #1a2744 30%, #0f1b36 70%, #0A192F 100%); + border-radius: 8px; + color: #00CED1; + font-weight: 700; + position: relative; + overflow: hidden; + box-shadow: + 0 0 8px rgba(0, 206, 209, 0.4), + 0 0 20px rgba(0, 206, 209, 0.15), + inset 0 1px 0 rgba(255, 255, 255, 0.1), + inset 0 0 20px rgba(0, 206, 209, 0.05); + transition: all 0.3s ease; + border: 1px solid rgba(0, 206, 209, 0.2); +} +.deep-dive-enhanced:hover { + transform: translateY(-1px); + box-shadow: + 0 0 12px rgba(0, 206, 209, 0.6), + 0 0 30px rgba(0, 206, 209, 0.25), + 0 2px 8px rgba(0, 0, 0, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.15), + inset 0 0 25px rgba(0, 206, 209, 0.08); + border-color: rgba(0, 206, 209, 0.4); +} +.deep-dive-enhanced::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(45deg, transparent, rgba(236, 240, 241, 0.08), transparent); + animation: shimmer 4s; +} +.deep-dive-enhanced::after { + content: ''; + position: absolute; + top: 2px; + left: 2px; + right: 2px; + bottom: 2px; + background: linear-gradient(135deg, rgba(0, 206, 209, 0.03) 0%, transparent 50%, rgba(0, 206, 209, 0.03) 100%); + border-radius: 6px; + pointer-events: none; +} +.icon-text { + position: relative; + z-index: 2; + text-shadow: 0 0 8px rgba(0, 206, 209, 0.5); + animation: pulse 4s ease-in-out infinite; +} +@keyframes shimmer { + 0% { + left: -100%; + } + 100% { + left: 100%; + } +} +@keyframes pulse { + 0%, 100% { + text-shadow: 0 0 8px rgba(0, 206, 209, 0.5); + } + 50% { + text-shadow: 0 0 12px rgba(0, 206, 209, 0.8), 0 0 20px rgba(0, 206, 209, 0.3); + } +} +/* Floating particles effect */ +.particles { + position: absolute; + width: 100%; + height: 100%; + pointer-events: none; + overflow: hidden; + border-radius: 12px; +} +.particle { + position: absolute; + width: 2px; + height: 2px; + background: #00CED1; + border-radius: 50%; + opacity: 0.6; + animation: float 6s linear infinite; +} +.particle:nth-child(1) { + left: 20%; + animation-delay: -1s; + animation-duration: 5s; +} +.particle:nth-child(2) { + left: 50%; + animation-delay: -2s; + animation-duration: 7s; +} +.particle:nth-child(3) { + left: 80%; + animation-delay: -3s; + animation-duration: 6s; +} +@keyframes float { + 0% { + transform: translateY(70px) scale(0); + opacity: 0; + } + 10% { + opacity: 0.6; + } + 90% { + opacity: 0.6; + } + 100% { + transform: translateY(-10px) scale(1); + opacity: 0; + } +} +/* Corner accents */ +.corner-accent { + position: absolute; + width: 6px; + height: 1px; + background: rgba(236, 240, 241, 0.3); + opacity: 0.8; +} +.corner-accent.top-left { + top: 4px; + left: 4px; + transform: rotate(45deg); +} +.corner-accent.bottom-right { + bottom: 4px; + right: 4px; + transform: rotate(45deg); +} +.corner-accent.top-right { + top: 4px; + right: 4px; + transform: rotate(-45deg); +} +.corner-accent.bottom-left { + bottom: 4px; + left: 4px; + transform: rotate(-45deg); +} diff --git a/public/favicon-test.html b/public/favicon-test.html new file mode 100644 index 0000000..c4f0473 --- /dev/null +++ b/public/favicon-test.html @@ -0,0 +1,367 @@ + + + + + + Header Layout Fix + + + + + + + + diff --git a/public/favicon.html b/public/favicon.html new file mode 100644 index 0000000..d7253dd --- /dev/null +++ b/public/favicon.html @@ -0,0 +1,95 @@ + + + + + +Lambda Favicon Concepts + + + + +
+
λ
+
Deep Dive Lambda
Dark navy background, glowing cyan λ
+
+ +
+
λ
+
Ghost Lambda
Light transparent background, solid cyan λ
+
+ +
+
λ
+
Circle of Logic
Blue circle, white/light cyan λ
+
+ + + diff --git a/public/favicons/favicon-16x16.png b/public/favicons/favicon-16x16.png new file mode 100644 index 0000000..b7d092a --- /dev/null +++ b/public/favicons/favicon-16x16.png Binary files differ diff --git a/public/favicons/favicon-192x192.png b/public/favicons/favicon-192x192.png new file mode 100644 index 0000000..827384a --- /dev/null +++ b/public/favicons/favicon-192x192.png Binary files differ diff --git a/public/favicons/favicon-32x32.png b/public/favicons/favicon-32x32.png new file mode 100644 index 0000000..4124f33 --- /dev/null +++ b/public/favicons/favicon-32x32.png Binary files differ diff --git a/public/favicons/favicon-48x48.png b/public/favicons/favicon-48x48.png new file mode 100644 index 0000000..374d10a --- /dev/null +++ b/public/favicons/favicon-48x48.png Binary files differ diff --git a/public/favicons/favicon-512.png b/public/favicons/favicon-512.png new file mode 100644 index 0000000..c98d309 --- /dev/null +++ b/public/favicons/favicon-512.png Binary files differ diff --git a/public/favicons/favicon-64x64.ico b/public/favicons/favicon-64x64.ico new file mode 100644 index 0000000..c9446f7 --- /dev/null +++ b/public/favicons/favicon-64x64.ico Binary files differ diff --git a/public/favicons/favicon-64x64.png b/public/favicons/favicon-64x64.png new file mode 100644 index 0000000..3c96b90 --- /dev/null +++ b/public/favicons/favicon-64x64.png Binary files differ diff --git a/public/favicons/favicon.ico b/public/favicons/favicon.ico new file mode 100644 index 0000000..afba688 --- /dev/null +++ b/public/favicons/favicon.ico Binary files differ diff --git a/public/header.html b/public/header.html new file mode 100644 index 0000000..401a18f --- /dev/null +++ b/public/header.html @@ -0,0 +1,360 @@ + + + + + + Background Pattern Options + + + +

Subtle Background Pattern Options

+

Each pattern uses low opacity (5-15%) to maintain readability while adding visual interest.

+ +
+
1. Geometric Grid Pattern
+
+
+
λ
+
Jason Poage
+
+ +
+
+ +
+
2. Dot Pattern
+
+
+
λ
+
Jason Poage
+
+ +
+
+ +
+
3. Lambda Watermarks
+
+
+
λ
+
Jason Poage
+
+ +
+
+ +
+
4. Hexagonal Tessellation
+
+
+
λ
+
Jason Poage
+
+ +
+
+ +
+
5. Circuit Board Pattern
+
+
+
λ
+
Jason Poage
+
+ +
+
+ +
+
6. Triangular Tessellation
+
+
+
λ
+
Jason Poage
+
+ +
+
+ +
+
7. Gradient Mesh Pattern
+
+
+
λ
+
Jason Poage
+
+ +
+
+ +
+
8. Topographical Lines
+
+
+
λ
+
Jason Poage
+
+ +
+
+ +
+
9. Polygon Shapes
+
+
+
λ
+
Jason Poage
+
+ +
+
+ +
+
10. Soft Bokeh Effect
+
+
+
λ
+
Jason Poage
+
+ +
+
+ + diff --git a/public/js/redirect.js b/public/js/redirect.js new file mode 100644 index 0000000..6ca98f9 --- /dev/null +++ b/public/js/redirect.js @@ -0,0 +1,12 @@ +document.addEventListener("DOMContentLoaded", function () { + const container = document.querySelector(".redirect-container"); + if (!container) return; + + const url = container.getAttribute("data-redirect-url"); + if (!url) return; + + // Redirect methods + setTimeout(() => (window.location.href = url), 100); + setTimeout(() => window.location.replace(url), 1000); + setTimeout(() => (document.location = url), 2000); +}); diff --git a/shell.nix b/shell.nix index 2261694..e34e8c4 100644 --- a/shell.nix +++ b/shell.nix @@ -4,15 +4,23 @@ { pkgs ? import {} }: pkgs.mkShell { - packages = [ - pkgs.which - pkgs.nodejs_latest - pkgs.nodePackages.pnpm - # pkgs.nodePackages.pm2 + packages = with pkgs; [ + which + nodejs_latest + nodePackages.pnpm + chromium + # nodePackages.pm2mk + imagemagick ]; shellHook = '' - export NODE_ENV=development + export PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true + export PUPPETEER_EXECUTABLE_PATH=${pkgs.chromium}/bin/chromium + alias mkicon="node src/render-favicon.js" + alias mkfavicons="node src/generate-favicon.js" + alias mkfavicon="magick static/favicons/favicon-512.png -define icon:auto-resize=64,48,32,16 static/favicons/favicon.ico" + echo "Type 'mkicon' to render the favicon-512.png" + echo "Type 'mkfavicon' to generate favicon.ico from favicon-512.png" + ''; } - diff --git a/src/api/posts.js b/src/api/posts.js deleted file mode 100644 index e69de29..0000000 --- a/src/api/posts.js +++ /dev/null diff --git a/src/app.js b/src/app.js index c7996f7..c2b0aab 100644 --- a/src/app.js +++ b/src/app.js @@ -1,28 +1,36 @@ // src/app.js -console.log("CWD:", process.cwd()); - require("dotenv").config(); + const setupMiddleware = require("./middleware"); - const { manualLogger } = require("./utils/logging"); -// const path = require("path"); - const { startTokenCleanup } = require("./utils/tokenCleanup"); + +const PORT = process.env.PORT || 3400; +const CWD_LOG = `CWD: ${process.cwd()}`; +const SERVER_LISTEN_LOG = (port) => + `Server listening on http://localhost:${port}`; +const NODE_ENV_LOG = `NODE_ENV: ${process.env.NODE_ENV}`; +const UNCUGHT_EXCEPTION_MSG = "Uncaught Exception:"; +const UNHANDLED_REJECTION_MSG = "Unhandled Rejection:"; + +function handleUncaughtException(err) { + manualLogger.error(UNCUGHT_EXCEPTION_MSG, err.stack || err); +} + +function handleUnhandledRejection(reason) { + manualLogger.error(UNHANDLED_REJECTION_MSG, reason?.stack || reason); +} + +console.log(CWD_LOG); + startTokenCleanup(); -app = setupMiddleware(); +const app = setupMiddleware(); -port = process.env.PORT || 3400; - -app.listen(port, () => { - console.log(`Server listening on http://localhost:${port}`); - console.log(`NODE_ENV: ${process.env.NODE_ENV}`); +app.listen(PORT, () => { + console.log(SERVER_LISTEN_LOG(PORT)); + console.log(NODE_ENV_LOG); }); -process.on("uncaughtException", (err) => { - manualLogger.error("Uncaught Exception:", err.stack || err); -}); - -process.on("unhandledRejection", (reason, promise) => { - manualLogger.error("Unhandled Rejection:", reason?.stack || reason); -}); +process.on("uncaughtException", handleUncaughtException); +process.on("unhandledRejection", handleUnhandledRejection); diff --git a/src/constants/authConstants.js b/src/constants/authConstants.js new file mode 100644 index 0000000..a3a0a18 --- /dev/null +++ b/src/constants/authConstants.js @@ -0,0 +1,16 @@ +// constants/authConstants.js +const VERIFY_URL = process.env.AUTH_VERIFY; +const CACHE_TTL = parseInt(process.env.AUTH_CACHE_TTL, 10) || 120000; // 2 minutes default +const AUTH_TIMEOUT_MS = 5000; // 5 second timeout + +const LOG_MESSAGES = { + AUTH_SERVER_UNAVAILABLE: + "[AuthCheck] Auth server unavailable, continuing unauthenticated", +}; + +module.exports = { + VERIFY_URL, + CACHE_TTL, + AUTH_TIMEOUT_MS, + LOG_MESSAGES, +}; diff --git a/src/constants/errorConstants.js b/src/constants/errorConstants.js new file mode 100644 index 0000000..21e37bf --- /dev/null +++ b/src/constants/errorConstants.js @@ -0,0 +1,14 @@ +const DEFAULT_ERROR_MESSAGE = "Internal Server Error"; +const DEFAULT_STACK_TRACE = "No stack trace available"; +const DEFAULT_STATUS_CODE = 500; +const DEFAULT_LOG_LEVEL = "error"; +const ERROR_VIEW = "pages/error"; +const ERROR_REDIRECT_PATH = "/error"; +module.exports = { + DEFAULT_ERROR_MESSAGE, + DEFAULT_STACK_TRACE, + DEFAULT_STATUS_CODE, + DEFAULT_LOG_LEVEL, + ERROR_VIEW, + ERROR_REDIRECT_PATH, +}; diff --git a/src/constants/hbsConstants.js b/src/constants/hbsConstants.js new file mode 100644 index 0000000..8e8cb8f --- /dev/null +++ b/src/constants/hbsConstants.js @@ -0,0 +1,20 @@ +// constants/hbsConstants.js +const VIEW_ENGINE = "handlebars"; +const LAYOUTS_DIR = "../views/layouts"; +const PARTIALS_DIR = "../views/partials"; +const DEFAULT_LAYOUT = "main"; +const EXTENSION = ".handlebars"; + +const RUNTIME_OPTIONS = { + allowProtoPropertiesByDefault: true, + allowProtoMethodsByDefault: true, +}; + +module.exports = { + VIEW_ENGINE, + LAYOUTS_DIR, + PARTIALS_DIR, + DEFAULT_LAYOUT, + EXTENSION, + RUNTIME_OPTIONS, +}; diff --git a/src/constants/htmlFormatConstants.js b/src/constants/htmlFormatConstants.js new file mode 100644 index 0000000..58a3b5c --- /dev/null +++ b/src/constants/htmlFormatConstants.js @@ -0,0 +1,15 @@ +// constants/htmlFormatConstants.js +const BEAUTIFY_OPTIONS = { + indent_size: 2, + wrap_line_length: 80, + end_with_newline: true, +}; + +const ERROR_MESSAGES = { + BEAUTIFY_ERROR: "Beautify error:", +}; + +module.exports = { + BEAUTIFY_OPTIONS, + ERROR_MESSAGES, +}; diff --git a/src/constants/httpLimits.js b/src/constants/httpLimits.js new file mode 100644 index 0000000..2f7878d --- /dev/null +++ b/src/constants/httpLimits.js @@ -0,0 +1,12 @@ +// constants/httpLimits.js +const ALLOWED_HTTP_METHODS = ["HEAD", "GET", "POST"]; +const MAX_HEADER_COUNT = 100; +const DISALLOWED_CONTENT_TYPE_SUBSTRINGS = ["multipart/form-data"]; +const MAX_CONTENT_LENGTH = 4096; + +module.exports = { + ALLOWED_HTTP_METHODS, + MAX_HEADER_COUNT, + DISALLOWED_CONTENT_TYPE_SUBSTRINGS, + MAX_CONTENT_LENGTH, +}; diff --git a/src/constants/httpMessages.js b/src/constants/httpMessages.js new file mode 100644 index 0000000..ce80266 --- /dev/null +++ b/src/constants/httpMessages.js @@ -0,0 +1,9 @@ +// constants/httpMessages.js +const HTTP_ERRORS = { + METHOD_NOT_ALLOWED: (method) => `Http Method '${method}' Not Allowed`, + PAYLOAD_TOO_LARGE: "Payload Too Large", + FILE_UPLOADS_NOT_ALLOWED: "File uploads are not allowed.", + TOO_MANY_HEADERS: "Too many headers.", +}; + +module.exports = { HTTP_ERRORS }; diff --git a/src/constants/middlewareConstants.js b/src/constants/middlewareConstants.js new file mode 100644 index 0000000..53fadf3 --- /dev/null +++ b/src/constants/middlewareConstants.js @@ -0,0 +1,17 @@ +const TRUST_PROXY = true; +const EXCLUDED_PATHS = ["/contact", "/analytics", "/track"]; +const DATA_LIMIT_BYTES = 10 * 1024; +const RAW_BODY_LIMIT_BYTES = 100 * 1024; +const RAW_BODY_TYPE = "*/*"; +const FALLBACK_ENCODING = "utf8"; +const FALLBACK_BODY = {}; + +module.exports = { + TRUST_PROXY, + EXCLUDED_PATHS, + DATA_LIMIT_BYTES, + RAW_BODY_LIMIT_BYTES, + RAW_BODY_TYPE, + FALLBACK_ENCODING, + FALLBACK_BODY, +}; diff --git a/src/constants/newsletterConstants.js b/src/constants/newsletterConstants.js new file mode 100644 index 0000000..bfeed51 --- /dev/null +++ b/src/constants/newsletterConstants.js @@ -0,0 +1,20 @@ +// constants/newsletterConstants.js +const FILE_PATH = require("path").join( + __dirname, + "../../data/newsletter-emails.json" +); + +const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + +const ERRORS = { + INVALID_EMAIL: "Invalid email format", + PARSE_FAILURE: "Failed to parse newsletter-emails.json", + WRITE_FAILURE: "writeFile failed", + SAVE_EMAIL_FAILURE: "Failed to save email", +}; + +module.exports = { + FILE_PATH, + EMAIL_REGEX, + ERRORS, +}; diff --git a/src/constants/rssConstants.js b/src/constants/rssConstants.js new file mode 100644 index 0000000..a7cd313 --- /dev/null +++ b/src/constants/rssConstants.js @@ -0,0 +1,10 @@ +// src/constants/rssConstants.js +const FEED_TITLE = "My Blog"; +const FEED_DESCRIPTION = "Latest posts from my blog"; +const FEED_LANGUAGE = "en"; + +module.exports = { + FEED_TITLE, + FEED_DESCRIPTION, + FEED_LANGUAGE, +}; diff --git a/src/constants/securityConstants.js b/src/constants/securityConstants.js new file mode 100644 index 0000000..abb2119 --- /dev/null +++ b/src/constants/securityConstants.js @@ -0,0 +1,24 @@ +// config/securityConstants.js + +module.exports = { + LOCALHOST_HOSTNAMES: ["127.0.0.1", "localhost"], + HEALTHCHECK_METHOD: "HEAD", + HEALTHCHECK_PATH: "/healthcheck", + FORBIDDEN_MESSAGE: "Forbidden", + FORBIDDEN_STATUS_CODE: 403, + HSTS_MAX_AGE: 63072000, + CSP_DIRECTIVES: { + defaultSrc: ["'self'"], + scriptSrc: ["'self'", "https://hcaptcha.com"], + styleSrc: ["'self'", "https:"], + imgSrc: [ + "'self'", + "data:", + "https://licensebuttons.net", + "https://cdn.jsdelivr.net", + ], + frameSrc: ["'self'", "https://newassets.hcaptcha.com"], + objectSrc: ["'none'"], + upgradeInsecureRequests: [], + }, +}; diff --git a/src/constants/sitemapConstants.js b/src/constants/sitemapConstants.js new file mode 100644 index 0000000..84ffdb1 --- /dev/null +++ b/src/constants/sitemapConstants.js @@ -0,0 +1,17 @@ +// constants/sitemapConstants.js +const STATIC_SITEMAP_PATH = "../../content/sitemap.json"; +const POSTS_PATH = "../../content/posts"; + +const DEFAULT_CHANGEFREQ = "monthly"; +const DEFAULT_PRIORITY = "0.5"; +const BLOG_POST_CHANGEFREQ = "monthly"; +const BLOG_POST_PRIORITY = "0.7"; + +module.exports = { + STATIC_SITEMAP_PATH, + POSTS_PATH, + DEFAULT_CHANGEFREQ, + DEFAULT_PRIORITY, + BLOG_POST_CHANGEFREQ, + BLOG_POST_PRIORITY, +}; diff --git a/src/css/base.css b/src/css/base.css index c892667..8b25fe3 100644 --- a/src/css/base.css +++ b/src/css/base.css @@ -14,3 +14,22 @@ padding: 1rem 1.5rem; } +.pattern-dots { + position: relative; + background-image: radial-gradient(circle, rgba(0,0,0,0.07) 1px, transparent 1px); + background-size: 15px 15px; + z-index: 0; +} + +.pattern-dots::after { + content: ""; + pointer-events: none; + position: absolute; + inset: 0; + background: linear-gradient(to bottom, rgba(255,255,255,0) 0%, rgba(255,255,255,1) 90%); +} + +.pattern-dots > * { + position: relative; + z-index: 1; +} diff --git a/src/css/favicon.css b/src/css/favicon.css new file mode 100644 index 0000000..6c988a5 --- /dev/null +++ b/src/css/favicon.css @@ -0,0 +1,172 @@ +.icon { + width: 64px; + height: 64px; + display: flex; + justify-content: center; + align-items: center; + font-size: 36px; + user-select: none; +} + +.deep-dive-enhanced { + background: linear-gradient(135deg, #0A192F 0%, #1a2744 30%, #0f1b36 70%, #0A192F 100%); + border-radius: 8px; + color: #00CED1; + font-weight: 700; + position: relative; + overflow: hidden; + box-shadow: + 0 0 8px rgba(0, 206, 209, 0.4), + 0 0 20px rgba(0, 206, 209, 0.15), + inset 0 1px 0 rgba(255, 255, 255, 0.1), + inset 0 0 20px rgba(0, 206, 209, 0.05); + transition: all 0.3s ease; + border: 1px solid rgba(0, 206, 209, 0.2); +} + +.deep-dive-enhanced:hover { + transform: translateY(-1px); + box-shadow: + 0 0 12px rgba(0, 206, 209, 0.6), + 0 0 30px rgba(0, 206, 209, 0.25), + 0 2px 8px rgba(0, 0, 0, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.15), + inset 0 0 25px rgba(0, 206, 209, 0.08); + border-color: rgba(0, 206, 209, 0.4); +} + +.deep-dive-enhanced::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(45deg, transparent, rgba(236, 240, 241, 0.08), transparent); + animation: shimmer 4s; +} + +.deep-dive-enhanced::after { + content: ''; + position: absolute; + top: 2px; + left: 2px; + right: 2px; + bottom: 2px; + background: linear-gradient(135deg, rgba(0, 206, 209, 0.03) 0%, transparent 50%, rgba(0, 206, 209, 0.03) 100%); + border-radius: 6px; + pointer-events: none; +} + +.icon-text { + position: relative; + z-index: 2; + text-shadow: 0 0 8px rgba(0, 206, 209, 0.5); + animation: pulse 4s ease-in-out infinite; +} + +@keyframes shimmer { + 0% { + left: -100%; + } + 100% { + left: 100%; + } +} + +@keyframes pulse { + 0%, 100% { + text-shadow: 0 0 8px rgba(0, 206, 209, 0.5); + } + 50% { + text-shadow: 0 0 12px rgba(0, 206, 209, 0.8), 0 0 20px rgba(0, 206, 209, 0.3); + } +} + +/* Floating particles effect */ +.particles { + position: absolute; + width: 100%; + height: 100%; + pointer-events: none; + overflow: hidden; + border-radius: 12px; +} + +.particle { + position: absolute; + width: 2px; + height: 2px; + background: #00CED1; + border-radius: 50%; + opacity: 0.6; + animation: float 6s linear infinite; +} + +.particle:nth-child(1) { + left: 20%; + animation-delay: -1s; + animation-duration: 5s; +} + +.particle:nth-child(2) { + left: 50%; + animation-delay: -2s; + animation-duration: 7s; +} + +.particle:nth-child(3) { + left: 80%; + animation-delay: -3s; + animation-duration: 6s; +} + +@keyframes float { + 0% { + transform: translateY(70px) scale(0); + opacity: 0; + } + 10% { + opacity: 0.6; + } + 90% { + opacity: 0.6; + } + 100% { + transform: translateY(-10px) scale(1); + opacity: 0; + } +} + +/* Corner accents */ +.corner-accent { + position: absolute; + width: 6px; + height: 1px; + background: rgba(236, 240, 241, 0.3); + opacity: 0.8; +} + +.corner-accent.top-left { + top: 4px; + left: 4px; + transform: rotate(45deg); +} + +.corner-accent.bottom-right { + bottom: 4px; + right: 4px; + transform: rotate(45deg); +} + +.corner-accent.top-right { + top: 4px; + right: 4px; + transform: rotate(-45deg); +} + +.corner-accent.bottom-left { + bottom: 4px; + left: 4px; + transform: rotate(-45deg); +} diff --git a/src/css/footer.css b/src/css/footer.css index cc264fe..8ac3094 100644 --- a/src/css/footer.css +++ b/src/css/footer.css @@ -49,10 +49,8 @@ margin-top: 0; margin-bottom: 0; display: flex; - gap: 1.5rem; /* Adjust this value to control spacing between links */ - /* If you want them centered within the nav */ + gap: 1.5rem; justify-content: center; - /* Allow items to wrap to the next line on smaller screens */ flex-wrap: wrap; } diff --git a/src/css/header.css b/src/css/header.css index 443b0c6..a6ec8b0 100644 --- a/src/css/header.css +++ b/src/css/header.css @@ -3,30 +3,57 @@ background-color: #2c3e50; color: #ecf0f1; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); + position:relative; + z-index: 10; } -header#site-header .container { +/* header#site-header .container { display: flex; flex-direction: column; align-items: flex-start; gap: 0.5rem; padding: 1rem 0; -} - -header#site-header .logo { - font-size: 1.8rem; - font-weight: bolder; +} */ +header#site-header .container { + max-width: 800px; + margin: 0 auto; + padding: 1rem 1.5rem; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; position: relative; } -header#site-header .logo a { +/* Logo section - positioned above nav */ +header#site-header .logo { + display: flex; + align-items: center; + font-size: 1.8rem; + font-weight: bolder; + position: relative; + text-decoration: none; + color: inherit; + gap: 1rem; + margin-left: 10px; + padding-left: 64px; /* Space for the icon */ +} + +header#site-header .site-title { color: inherit; text-decoration: none; position: relative; display: inline-block; } -header#site-header .logo a::after { +header#site-header .site-title, a { + color: inherit; + text-decoration: none; + position: relative; + display: inline-block; +} + +header#site-header .site-title a::after { content: ""; position: absolute; left: 0; @@ -39,7 +66,33 @@ transition: transform 0.3s ease; } -header#site-header .logo a:hover::after { +header#site-header .site-title a:hover::after { transform: scaleX(1); } +.pattern-lambda { + background-image: + repeating-linear-gradient( + 45deg, + transparent, + transparent 50px, + rgba(236, 240, 241, 0.08) 50px, + rgba(236, 240, 241, 0.08) 52px + ); + position: relative; +} + +.pattern-lambda::before { + content: ' '; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + font-size: 2rem; + color: rgba(236, 240, 241, 0.05); + letter-spacing: 2rem; + line-height: 3rem; + overflow: hidden; + pointer-events: none; +} diff --git a/src/css/layout.css b/src/css/layout.css deleted file mode 100644 index c8794ec..0000000 --- a/src/css/layout.css +++ /dev/null @@ -1,7 +0,0 @@ -.layout { - display: flex; - max-width: 1200px; - margin: 2rem auto; - padding: 0 1.5rem; - gap: 2rem; -} diff --git a/src/css/nav.css b/src/css/nav.css index 86dfc58..86ebc26 100644 --- a/src/css/nav.css +++ b/src/css/nav.css @@ -1,8 +1,9 @@ header#site-header nav.site-nav { display: flex; gap: 1rem; + position: relative; + z-index: 10; } - header#site-header nav.site-nav a, nav.site-nav .dropdown-content form button { text-decoration: none; @@ -40,6 +41,7 @@ nav.site-nav .dropdown { position: relative; display: block; + z-index: 11; } nav.site-nav .dropbtn { @@ -48,11 +50,11 @@ } nav.site-nav .dropdown-content { + z-index: 11; display: none; position: absolute; background-color: white; min-width: 160px; - z-index: 1; box-shadow: 0px 8px 16px rgba(0,0,0,0.2); } @@ -85,3 +87,24 @@ nav.site-nav .dropdown-content form { margin: 0; } + +/* Navigation row with consistent left alignment */ +.nav-row { + display: flex; + align-items: center; + gap: 1rem; + width: 100%; + padding-left: 64px; /* Same as icon width + gap to align with site title */ +} + +/* Logo icon that acts as home link - spans from site title to nav */ +.nav-logo { + text-decoration: none; + color: inherit; + display: flex; + align-items: flex-end; + position: absolute; + left: 1rem; + top: 1rem; + bottom: 1rem; +} diff --git a/src/css/responsive.css b/src/css/responsive.css index b41fcad..c264a0b 100644 --- a/src/css/responsive.css +++ b/src/css/responsive.css @@ -57,6 +57,12 @@ .sidebar { width: 100%; } + header#site-header .container { + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + } + header#site-header nav.site-nav { flex-direction: column; gap: 0; @@ -78,6 +84,22 @@ header#site-header #siteNav.hide a { display: none; } + nav.site-nav { + flex-direction: column; + gap: 0; + width: 100%; + } + .nav-row { + flex-direction: column; + align-items: flex-start; + width: 100%; + padding-left: 0; + } + .nav-logo { + position: relative; + left: 0; + margin-bottom: 0.5rem; + } nav.site-nav ul { display: block; justify-content: center; diff --git a/src/css/styles.css b/src/css/styles.css index 190f3cb..863852d 100644 --- a/src/css/styles.css +++ b/src/css/styles.css @@ -1,10 +1,10 @@ @import url('base.css'); @import url('footer.css'); @import url('header.css'); -@import url('layout.css'); @import url('main.css'); @import url('nav.css'); @import url('reset.css'); @import url('sidebar.css'); @import url('toc.css'); @import url('responsive.css'); +@import url('favicon.css'); diff --git a/src/favicon-animation.html b/src/favicon-animation.html new file mode 100644 index 0000000..193ad7c --- /dev/null +++ b/src/favicon-animation.html @@ -0,0 +1,240 @@ + + + + + + + + + + +
+
+
+
+
+
+
+
+
+
+ λ +
+ + diff --git a/src/favicon-template.html b/src/favicon-template.html new file mode 100644 index 0000000..798cbe6 --- /dev/null +++ b/src/favicon-template.html @@ -0,0 +1,248 @@ + + + + + + + + + + +
+
+
+
+
+
+
+
+
+
+ λ +
+ + diff --git a/src/generate-favicon.js b/src/generate-favicon.js new file mode 100644 index 0000000..dc6ecb3 --- /dev/null +++ b/src/generate-favicon.js @@ -0,0 +1,31 @@ +const sharp = require("sharp"); +const fs = require("fs"); +const path = require("path"); + +const source = path.resolve(__dirname, "../static/favicons/favicon-512.png"); +const outputDir = path.resolve(__dirname, "../static/favicons"); + +const sizes = [16, 32, 48, 64, 192]; +sizes.forEach((size) => { + sharp(source) + .resize(size, size) + .toFile(path.join(outputDir, `favicon-${size}x${size}.png`)) + .catch(console.error); +}); + +sharp(source) + .resize(48, 48) + .toFile(path.join(outputDir, "favicon-48x48.png")) + .then(() => { + const ico = require("to-ico"); + const buffers = [ + fs.readFileSync(path.join(outputDir, "favicon-16x16.png")), + fs.readFileSync(path.join(outputDir, "favicon-32x32.png")), + fs.readFileSync(path.join(outputDir, "favicon-48x48.png")), + ]; + return ico(buffers); + }) + .then((buf) => { + fs.writeFileSync(path.join(outputDir, "favicon.ico"), buf); + }) + .catch(console.error); diff --git a/src/middleware/applyProductionSecurity.js b/src/middleware/applyProductionSecurity.js index cb3e8d2..e813ec9 100644 --- a/src/middleware/applyProductionSecurity.js +++ b/src/middleware/applyProductionSecurity.js @@ -3,51 +3,50 @@ const xssSanitizer = require("./xssSanitizer"); const HttpError = require("../utils/HttpError"); const { baseUrl } = require("../utils/baseUrl"); +const { + LOCALHOST_HOSTNAMES, + HEALTHCHECK_METHOD, + HEALTHCHECK_PATH, + FORBIDDEN_MESSAGE, + FORBIDDEN_STATUS_CODE, + HSTS_MAX_AGE, + CSP_DIRECTIVES, +} = require("../constants/securityConstants"); + +const disablePoweredBy = (req, res, next) => { + req.app.disable("x-powered-by"); + next(); +}; + +const logIps = (req, res, next) => { + const forwardedIp = req.ip; + const directIp = req.connection.remoteAddress; + req.log?.info?.(`Forwarded IP: ${forwardedIp}`); + req.log?.info?.(`Direct IP: ${directIp}`); + next(); +}; + +const blockLocalhostAccess = (req, res, next) => { + if (req.method === HEALTHCHECK_METHOD && req.path === HEALTHCHECK_PATH) { + return next(); + } + if (LOCALHOST_HOSTNAMES.includes(req.hostname)) { + req.log.info(`Method: ${req.method} Path ${req.path}`); + return next(new HttpError(FORBIDDEN_MESSAGE, FORBIDDEN_STATUS_CODE)); + } + next(); +}; const applyProductionSecurity = [ - (req, res, next) => { - req.app.disable("x-powered-by"); - next(); - }, - (req, res, next) => { - const forwardedIp = req.ip; - const directIp = req.connection.remoteAddress; - - req.log?.info?.(`Forwarded IP: ${forwardedIp}`); - req.log?.info?.(`Direct IP: ${directIp}`); - next(); - }, + disablePoweredBy, + logIps, hpp(), xssSanitizer, // rateLimit middleware can be added here - (req, res, next) => { - const isHealthcheck = req.method === "HEAD" && req.path === "/healthcheck"; - if (isHealthcheck) return next(); - - const host = req.hostname; - if (["127.0.0.1", "localhost"].includes(host)) { - req.log.info(`Method: ${req.method} Path ${req.path}`); - return next(new HttpError("Forbidden", 403)); - } - - next(); - }, - helmet.hsts({ maxAge: 63072000 }), + blockLocalhostAccess, + helmet.hsts({ maxAge: HSTS_MAX_AGE }), helmet.contentSecurityPolicy({ - directives: { - defaultSrc: ["'self'", baseUrl], - scriptSrc: ["'self'", "https://hcaptcha.com"], - styleSrc: ["'self'", "https:"], - imgSrc: [ - "'self'", - "data:", - "https://licensebuttons.net", - "https://cdn.jsdelivr.net", - ], - frameSrc: ["'self'", "https://newassets.hcaptcha.com"], - objectSrc: ["'none'"], - upgradeInsecureRequests: [], - }, + directives: { ...CSP_DIRECTIVES, defaultSrc: ["'self'", baseUrl] }, }), ]; diff --git a/src/middleware/authCheck.js b/src/middleware/authCheck.js index 4660673..1f1432c 100644 --- a/src/middleware/authCheck.js +++ b/src/middleware/authCheck.js @@ -1,23 +1,23 @@ // middleware/authCheck.js const fetch = require("node-fetch"); - -const VERIFY_URL = process.env.AUTH_VERIFY; -const CACHE_TTL = parseInt(process.env.AUTH_CACHE_TTL) || 120000; // 2 minutes default +const { + VERIFY_URL, + CACHE_TTL, + AUTH_TIMEOUT_MS, + LOG_MESSAGES, +} = require("../constants/authConstants"); // Simple in-memory cache const authCache = new Map(); -// Helper to generate cache key function getCacheKey(cookie, authHeader) { return `${cookie}:${authHeader}`; } -// Helper to check if cache entry is valid function isCacheValid(entry) { return entry && Date.now() - entry.timestamp < CACHE_TTL; } -// Clean expired cache entries periodically setInterval(() => { const now = Date.now(); for (const [key, entry] of authCache.entries()) { @@ -25,60 +25,45 @@ authCache.delete(key); } } -}, CACHE_TTL); // Clean up when entries would expire +}, CACHE_TTL); module.exports = async (req, res, next) => { const cookie = req.headers["cookie"] || ""; const authHeader = req.headers["authorization"] || ""; const cacheKey = getCacheKey(cookie, authHeader); - // Check cache first const cached = authCache.get(cacheKey); if (isCacheValid(cached)) { req.isAuthenticated = cached.isAuthenticated; return next(); } - // Default to unauthenticated req.isAuthenticated = false; try { const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), 5000); // 5 second timeout + const timeout = setTimeout(() => controller.abort(), AUTH_TIMEOUT_MS); const resVerify = await fetch(VERIFY_URL, { - headers: { - cookie, - authorization: authHeader, - }, + headers: { cookie, authorization: authHeader }, credentials: "include", signal: controller.signal, }); clearTimeout(timeout); - const isAuthenticated = resVerify.status === 200; + req.isAuthenticated = resVerify.status === 200; - // Cache the result authCache.set(cacheKey, { - isAuthenticated, + isAuthenticated: req.isAuthenticated, timestamp: Date.now(), }); - - req.isAuthenticated = isAuthenticated; - } catch (err) { - // Auth server down/timeout - silently fail, don't crash the app + } catch { req.isAuthenticated = false; - - // Optional: Log for debugging, but don't spam logs if (req.log) { - req.log.warn( - "[AuthCheck] Auth server unavailable, continuing unauthenticated" - ); + req.log.warn(LOG_MESSAGES.AUTH_SERVER_UNAVAILABLE); } else { - console.warn( - "[AuthCheck] Auth server unavailable, continuing unauthenticated" - ); + console.warn(LOG_MESSAGES.AUTH_SERVER_UNAVAILABLE); } } diff --git a/src/middleware/errorHandler.js b/src/middleware/errorHandler.js index c89f220..8ad85cb 100644 --- a/src/middleware/errorHandler.js +++ b/src/middleware/errorHandler.js @@ -4,18 +4,26 @@ const { getErrorContext } = require("../utils/errorContext"); const { buildErrorRenderContext } = require("../utils/buildErrorRenderContext"); const { isDev } = require("../utils/env"); +const { + DEFAULT_ERROR_MESSAGE, + DEFAULT_STACK_TRACE, + DEFAULT_STATUS_CODE, + DEFAULT_LOG_LEVEL, + ERROR_VIEW, + ERROR_REDIRECT_PATH, +} = require("../constants/errorConstants"); module.exports = async (err, req, res, next) => { - const statusCode = err.statusCode ?? 500; - const message = err.message ?? "Internal Server Error"; - const stack = err.stack ?? "No stack trace available"; + const statusCode = err.statusCode ?? DEFAULT_STATUS_CODE; + const message = err.message ?? DEFAULT_ERROR_MESSAGE; + const stack = err.stack ?? DEFAULT_STACK_TRACE; const code = err.code ?? null; const requestId = crypto.randomUUID?.() ?? Date.now().toString(36); const timestamp = new Date().toISOString(); const logEntry = { timestamp, - level: "error", + level: DEFAULT_LOG_LEVEL, requestId, method: req.method, url: req.originalUrl || req.url, @@ -38,7 +46,7 @@ const errorContext = getErrorContext(code || statusCode); if (!isDev) { - res.redirect(`/error?code=${errorContext.statusCode}`); + res.redirect(`${ERROR_REDIRECT_PATH}?code=${errorContext.statusCode}`); return; } @@ -54,5 +62,5 @@ }); const errorPageContext = await getBaseContext(req?.isAuthenticated, context); - res.status(errorContext.statusCode).render("pages/error", errorPageContext); + res.status(errorContext.statusCode).render(ERROR_VIEW, errorPageContext); }; diff --git a/src/middleware/formatHtml.js b/src/middleware/formatHtml.js index 8c02422..f842f80 100644 --- a/src/middleware/formatHtml.js +++ b/src/middleware/formatHtml.js @@ -1,22 +1,24 @@ // src/middleware/formatHtml.js const beautify = require("js-beautify").html; +const { + BEAUTIFY_OPTIONS, + ERROR_MESSAGES, +} = require("../constants/htmlFormatConstants"); module.exports = function (req, res, next) { const originalSend = res.send; res.send = function (body) { const contentType = res.get("Content-Type") || ""; - const isHTML = contentType.includes("text/html") || typeof body === "string" && body.trim().startsWith("<"); + const isHTML = + contentType.includes("text/html") || + (typeof body === "string" && body.trim().startsWith("<")); if (isHTML) { try { - body = beautify(body, { - indent_size: 2, - wrap_line_length: 80, - end_with_newline: true, - }); + body = beautify(body, BEAUTIFY_OPTIONS); } catch (e) { - console.error("Beautify error:", e); + console.error(ERROR_MESSAGES.BEAUTIFY_ERROR, e); } } @@ -25,4 +27,3 @@ next(); }; - diff --git a/src/middleware/hbs.js b/src/middleware/hbs.js index 08f9660..578db28 100644 --- a/src/middleware/hbs.js +++ b/src/middleware/hbs.js @@ -2,13 +2,21 @@ const path = require("path"); const exphbs = require("express-handlebars"); const { registerHelpers } = require("../utils/hbsHelpers"); +const { + VIEW_ENGINE, + LAYOUTS_DIR, + PARTIALS_DIR, + DEFAULT_LAYOUT, + EXTENSION, + RUNTIME_OPTIONS, +} = require("../constants/hbsConstants"); const hbsMiddleware = (req, res, next) => { if (!req.app.get("view engine")) { const hbs = exphbs.create({ - layoutsDir: path.join(__dirname, "../views/layouts"), - partialsDir: path.join(__dirname, "../views/partials"), - defaultLayout: "main", + layoutsDir: path.join(__dirname, LAYOUTS_DIR), + partialsDir: path.join(__dirname, PARTIALS_DIR), + defaultLayout: DEFAULT_LAYOUT, helpers: { section(name, options) { this._sections ??= {}; @@ -18,16 +26,13 @@ return null; }, }, - extname: ".handlebars", - runtimeOptions: { - allowProtoPropertiesByDefault: true, - allowProtoMethodsByDefault: true, - }, + extname: EXTENSION, + runtimeOptions: RUNTIME_OPTIONS, }); registerHelpers(hbs); - req.app.engine("handlebars", hbs.engine); - req.app.set("view engine", "handlebars"); + req.app.engine(VIEW_ENGINE, hbs.engine); + req.app.set("view engine", VIEW_ENGINE); req.app.set("views", path.join(__dirname, "../views")); } diff --git a/src/middleware/index.js b/src/middleware/index.js index 465f864..393e2ab 100644 --- a/src/middleware/index.js +++ b/src/middleware/index.js @@ -12,6 +12,7 @@ const baseContext = require("./baseContext"); const hbs = require("./hbs"); const authCheck = require("./authCheck"); +const { redirectMiddleware } = require("./redirect"); const { loggingMiddleware, @@ -22,13 +23,20 @@ function setupApp() { const app = express(); - const excludedPaths = ["/contact", "/analytics", "/track"]; - const DATA_LIMIT_BYTES = 10 * 1024; // 10k - app.set("trust proxy", true); + const { + TRUST_PROXY, + EXCLUDED_PATHS, + DATA_LIMIT_BYTES, + RAW_BODY_LIMIT_BYTES, + RAW_BODY_TYPE, + FALLBACK_ENCODING, + FALLBACK_BODY, + } = require("../constants/middlewareConstants"); + app.set("trust proxy", TRUST_PROXY); // General parsers for non-excluded routes app.use((req, res, next) => { - if (excludedPaths.includes(req.path)) return next(); + if (EXCLUDED_PATHS.includes(req.path)) return next(); express.json({ limit: DATA_LIMIT_BYTES })(req, res, (err) => { if (err) return next(err); express.urlencoded({ extended: false, limit: DATA_LIMIT_BYTES })( @@ -40,17 +48,20 @@ }); // Raw parser + manual truncation for excluded routes - const rawBodyParser = express.raw({ type: "*/*", limit: "100kb" }); + const rawBodyParser = express.raw({ + type: RAW_BODY_TYPE, + limit: RAW_BODY_LIMIT_BYTES, + }); app.use((req, res, next) => { - if (!excludedPaths.includes(req.path)) return next(); + if (!EXCLUDED_PATHS.includes(req.path)) return next(); rawBodyParser(req, res, (err) => { if (err) return next(err); try { - const raw = req.body.toString("utf8"); + const raw = req.body.toString(FALLBACK_ENCODING); const truncated = raw.slice(0, DATA_LIMIT_BYTES); req.body = JSON.parse(truncated); } catch (e) { - req.body = {}; // Fallback on parse failure + req.body = FALLBACK_BODY; // Fallback on parse failure } next(); }); @@ -76,6 +87,7 @@ app.use(compression()); app.use(validateRequestIntegrity); app.use(formatHtml); + app.use(redirectMiddleware); app.use(routes); app.use(errorHandler); return app; diff --git a/src/middleware/redirect.js b/src/middleware/redirect.js new file mode 100644 index 0000000..da79b8d --- /dev/null +++ b/src/middleware/redirect.js @@ -0,0 +1,68 @@ +// src/middleware/redirect.js + +const { baseUrl } = require("../utils/baseUrl"); + +// Configuration - adjust these as needed +const redirectConfig = { + "/": `${baseUrl}/blog`, + // Add more redirects as needed + // '/old-path': '/new-path', +}; + +// Helper function to build full URL +function buildRedirectUrl(req, targetPath) { + const protocol = req.get("x-forwarded-proto") || req.protocol; + const host = req.get("host"); + + // If targetPath is already a full URL, return it as-is + if (targetPath.startsWith("http")) { + return targetPath; + } + + // Build full URL + return `${protocol}://${host}${targetPath}`; +} + +// Generic redirect handler +function handleRedirect(req, res, targetPath) { + const redirectUrl = buildRedirectUrl(req, targetPath); + + // Log the redirect for debugging + console.log(`[REDIRECT] ${req.originalUrl} -> ${redirectUrl}`); + + // Check if this is a request that expects JSON (API calls) + if (req.accepts("json") && !req.accepts("html")) { + return res.status(301).json({ + redirect: true, + url: redirectUrl, + }); + } + + res.set("Location", redirectUrl); + + // For browsers, render the redirect page + res.status(301); + res.renderWithBaseContext("pages/redirect", { + redirectUrl: redirectUrl, + originalUrl: req.originalUrl, + }); +} + +// Middleware function to check for redirects +function redirectMiddleware(req, res, next) { + const targetPath = redirectConfig[req.path]; + + if (targetPath) { + return handleRedirect(req, res, targetPath); + } + + // No redirect needed, continue to next middleware + next(); +} + +// Export the middleware and utility functions +module.exports = { + redirectMiddleware, + handleRedirect, + redirectConfig, +}; diff --git a/src/middleware/routesList.js b/src/middleware/routesList.js new file mode 100644 index 0000000..277b2e0 --- /dev/null +++ b/src/middleware/routesList.js @@ -0,0 +1,79 @@ +// src/middleware/routesList.js +let cachedRoutes = null; +let cachedApp = null; + +function getAllRoutePaths(app) { + const paths = new Set(); + + function extractPaths(stack, basePath = "") { + if (!stack) return; + + stack.forEach((layer) => { + if (layer.route) { + // Direct route + paths.add(basePath + layer.route.path); + } else if ( + layer.name === "router" && + layer.handle && + layer.handle.stack + ) { + // Router middleware - try to extract base path + let routerPath = ""; + + // Try to extract path from regexp + if (layer.regexp && layer.regexp.source) { + const match = layer.regexp.source.match(/^\^\\?\/?([^\\$?]+)/); + if (match && match[1]) { + routerPath = "/" + match[1].replace(/\\\//g, "/"); + } + } + + extractPaths(layer.handle.stack, basePath + routerPath); + } + }); + } + + if (app && app._router && app._router.stack) { + extractPaths(app._router.stack); + } + + return Array.from(paths).sort(); +} + +// Middleware to capture the app instance +function routesList(req, res, next) { + // Store app reference for later use + if (!cachedApp && req.app) { + cachedApp = req.app; + } + next(); +} + +// Function to get routes (called from the route handler) +function getRoutes() { + if (!cachedApp) { + return []; + } + + // Cache routes on first access + if (!cachedRoutes) { + cachedRoutes = getAllRoutePaths(cachedApp); + } + + return cachedRoutes; +} + +// Force refresh of cached routes +function refreshRoutes() { + cachedRoutes = null; + if (cachedApp) { + cachedRoutes = getAllRoutePaths(cachedApp); + } + return cachedRoutes || []; +} + +module.exports = { + routesList, + getRoutes, + refreshRoutes, +}; diff --git a/src/middleware/validateRequestIntegrity.js b/src/middleware/validateRequestIntegrity.js index 8c49d4b..5972701 100644 --- a/src/middleware/validateRequestIntegrity.js +++ b/src/middleware/validateRequestIntegrity.js @@ -1,24 +1,32 @@ +// middleware/validateHttpRequest.js const HttpError = require("../utils/HttpError"); +const { + ALLOWED_HTTP_METHODS, + MAX_HEADER_COUNT, + DISALLOWED_CONTENT_TYPE_SUBSTRINGS, + MAX_CONTENT_LENGTH, +} = require("../constants/httpLimits"); +const { HTTP_ERRORS } = require("../constants/httpMessages"); + module.exports = (req, res, next) => { - const allowedMethods = ["HEAD", "GET", "POST"]; const contentLength = parseInt(req.get("content-length") || "0", 10); const contentType = req.headers["content-type"] || ""; const headerCount = Object.keys(req.headers).length; - if (!allowedMethods.includes(req.method)) { - return next(new HttpError(`Http Method '${req.method}' Not Allowed`, 405)); + if (!ALLOWED_HTTP_METHODS.includes(req.method)) { + return next(new HttpError(HTTP_ERRORS.METHOD_NOT_ALLOWED(req.method), 405)); } - if (contentLength > 4096) { - return next(new HttpError("Payload Too Large", 413)); + if (contentLength > MAX_CONTENT_LENGTH) { + return next(new HttpError(HTTP_ERRORS.PAYLOAD_TOO_LARGE, 413)); } - if (contentType.includes("multipart/form-data")) { - return next(new HttpError("File uploads are not allowed.", 400)); + if (DISALLOWED_CONTENT_TYPE_SUBSTRINGS.some((t) => contentType.includes(t))) { + return next(new HttpError(HTTP_ERRORS.FILE_UPLOADS_NOT_ALLOWED, 400)); } - if (headerCount > 100) { - return next(new HttpError("Too many headers.", 400)); + if (headerCount > MAX_HEADER_COUNT) { + return next(new HttpError(HTTP_ERRORS.TOO_MANY_HEADERS, 400)); } next(); diff --git a/src/presentation/postComponent.js b/src/presentation/postComponent.js deleted file mode 100644 index e69de29..0000000 --- a/src/presentation/postComponent.js +++ /dev/null diff --git a/src/presentation/render.js b/src/presentation/render.js deleted file mode 100644 index e69de29..0000000 --- a/src/presentation/render.js +++ /dev/null diff --git a/src/render-favicon.js b/src/render-favicon.js new file mode 100644 index 0000000..24d87a2 --- /dev/null +++ b/src/render-favicon.js @@ -0,0 +1,28 @@ +const puppeteer = require("puppeteer"); +const fs = require("fs"); +const path = require("path"); + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + + const htmlPath = path.resolve(__dirname, "favicon-template.html"); + const content = fs.readFileSync(htmlPath, "utf8"); + + await page.setContent(content, { waitUntil: "networkidle0" }); + await page.setViewport({ width: 512, height: 512, deviceScaleFactor: 1 }); + + const element = await page.$(".icon"); + + const outputDir = path.resolve(__dirname, "../static/favicons"); + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir); + } + + await element.screenshot({ + path: path.join(outputDir, "favicon-512.png"), + omitBackground: true, + }); + + await browser.close(); +})(); diff --git a/src/routes/index.js b/src/routes/index.js index 6902e98..40d1d4f 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -1,6 +1,7 @@ // src/routes/index.js const express = require("express"); const router = express.Router(); +const path = require("path"); const analytics = require("./analytics"); const robots = require("./robots"); @@ -14,18 +15,25 @@ const post = require("./post"); const pages = require("./pages"); const rssFeed = require("./rssFeed"); -const logs = require("./logs"); +// const logs = require("./logs"); const { qualifyLink } = require("../utils/qualifyLinks"); const HttpError = require("../utils/HttpError"); +const securedMiddleware = require("../middleware/secured"); +const securedRoutes = require("./secured"); + +const favicon = require("serve-favicon"); +const faviconsPath = path.join(__dirname, "..", "..", "public", "favicons"); +const faviconFile = path.resolve(faviconsPath, "favicon.ico"); + +router.use(securedMiddleware, securedRoutes); + router.get("/error", errorPage); // Landing page after error is logged -router.get("/favicon.ico", (req, res) => res.status(204).end()); router.head("/healthcheck", (req, res) => { res.sendStatus(200); }); -router.use(logs); router.use(admin); router.post("/track", analytics); @@ -49,7 +57,8 @@ }, }) ); -router.get("/favicon.ico", (req, res) => res.status(204).end()); +router.use("/favicons", express.static(faviconsPath)); +router.use(favicon(faviconFile)); router.use(blog_index); router.use(robots); @@ -60,13 +69,59 @@ router.get("/blog/:year/:month/:name", post); +// function flattenRouterLayers(stack, acc = []) { +// for (const layer of stack) { +// acc.push(layer); +// const h = layer.handle; +// if (typeof h === "function") { +// if (h.stack && Array.isArray(h.stack)) { +// flattenRouterLayers(h.stack, acc); +// } else if (h.handle && h.handle.stack && Array.isArray(h.handle.stack)) { +// flattenRouterLayers(h.handle.stack, acc); +// } +// } +// } +// return acc; +// } + +// router.use((req, res) => { +// const rootStack = req.app._router?.stack || req.app.router?.stack; +// if (!rootStack) return res.sendStatus(500); +// const flat = flattenRouterLayers(rootStack); +// const routes = []; +// flat.forEach((l) => { +// if (l.route) { +// routes.push(l.route.path); +// } +// }); +// res.json(routes).send(200); +// }); + +// router.use((req, res) => { +// const appStack = req.app._router?.stack || req.app.router?.stack; +// if (!appStack) return res.sendStatus(500); +// const flatStack = flattenRouterStack(appStack); +// flatStack.forEach((layer) => { +// console.log(layer); +// }); +// res.sendStatus(200); +// }); + router.get("/", (req, res) => { console.log(qualifyLink("/blog")); - res.redirect(301, qualifyLink("/blog")); + // res.redirect(301, qualifyLink("/blog")); + res.redirect(301, "/blog"); }); -router.use((req, res, next) => { - next(new HttpError(null, 404)); -}); +router.use( + (req, res, next) => { + console.log(path.join(__dirname, "static/favicons")); + next(); + }, + (req, res, next) => { + console.log(req.url); + next(new HttpError("Page not found", 404)); + } +); module.exports = router; diff --git a/src/routes/logs.js b/src/routes/logs.js deleted file mode 100644 index ce5a233..0000000 --- a/src/routes/logs.js +++ /dev/null @@ -1,150 +0,0 @@ -const express = require("express"); -const router = express.Router(); -const Database = require("better-sqlite3"); -const path = require("path"); -const fs = require("fs"); -const secured = require("../middleware/secured"); - -const allowedLevels = ["warn", "error", "info", "debug", "functions", "notice"]; - -const dbPath = path.resolve(__dirname, "../../data/logs.sqlite3"); - -if (!fs.existsSync(dbPath)) { - fs.closeSync(fs.openSync(dbPath, "w")); -} - -const db = new Database(dbPath, { readonly: true }); - -router.get("/logs", secured, (req, res) => { - res.renderWithBaseContext("pages/logs", { - showSidebar: false, - showFooter: false, - }); -}); - -router.post("/logs", secured, (req, res) => { - const start = process.hrtime.bigint(); - - const log_level = req.query.log_level || "*"; - const date = req.query.date || "*"; - const page = parseInt(req.query.page) || 1; - const limit = parseInt(req.query.limit) || 50; - const offset = (page - 1) * limit; - - const parseStart = process.hrtime.bigint(); - - if (log_level !== "*" && !allowedLevels.includes(log_level)) { - return res.status(400).json({ error: "Invalid log_level" }); - } - - const conditions = []; - const params = []; - - if (log_level !== "*") { - conditions.push("level = ?"); - params.push(log_level); - } - - if (date !== "*") { - conditions.push("date(timestamp) = ?"); - params.push(date); - } - - const whereClause = conditions.length - ? "WHERE " + conditions.join(" AND ") - : ""; - - const countStart = process.hrtime.bigint(); - - // Count query - simple and fast - const countQuery = `SELECT COUNT(*) as total FROM logs ${whereClause}`; - const totalResult = db.prepare(countQuery).get(...params); - const total = totalResult.total; - - const queryStart = process.hrtime.bigint(); - - // STEP 1: Get just the log records we need (fast!) - const logQuery = ` - SELECT id, timestamp, level - FROM logs - ${whereClause} - ORDER BY timestamp DESC - LIMIT ? OFFSET ? - `; - - try { - const logRows = db.prepare(logQuery).all(...params, limit, offset); - - if (logRows.length === 0) { - return res.json({ - logs: [], - pagination: { page, limit, total, totalPages: 0, hasMore: false }, - }); - } - - // STEP 2: Get metadata only for these specific logs - const logIds = logRows.map((row) => row.id); - const placeholders = logIds.map(() => "?").join(","); - - const metadataQuery = ` - SELECT - m.log_id, - k.key, - m.value - FROM log_metadata m - JOIN keys k ON k.id = m.key_id - WHERE m.log_id IN (${placeholders}) - `; - - const metadataRows = db.prepare(metadataQuery).all(...logIds); - - const mapStart = process.hrtime.bigint(); - - // STEP 3: Build metadata lookup map - const metadataMap = {}; - metadataRows.forEach((row) => { - if (!metadataMap[row.log_id]) { - metadataMap[row.log_id] = {}; - } - try { - metadataMap[row.log_id][row.key] = JSON.parse(row.value); - } catch { - metadataMap[row.log_id][row.key] = row.value; - } - }); - - // STEP 4: Combine logs with their metadata - const logs = logRows.map((row) => ({ - id: row.id, - timestamp: row.timestamp, - level: row.level, - ...(metadataMap[row.id] || {}), - })); - - const end = process.hrtime.bigint(); - - req.log.info("logs route timings", { - totalMs: Number(end - start) / 1e6, - parseMs: Number(parseStart - start) / 1e6, - countMs: Number(queryStart - countStart) / 1e6, - queryMs: Number(mapStart - queryStart) / 1e6, - mapMs: Number(end - mapStart) / 1e6, - }); - - res.json({ - logs, - pagination: { - page, - limit, - total, - totalPages: Math.ceil(total / limit), - hasMore: page < Math.ceil(total / limit), - }, - }); - } catch (error) { - console.error("Query error:", error); - res.status(500).json({ error: "Failed to query logs" }); - } -}); - -module.exports = router; diff --git a/src/routes/secured/filteredLogs.js b/src/routes/secured/filteredLogs.js new file mode 100644 index 0000000..6b76936 --- /dev/null +++ b/src/routes/secured/filteredLogs.js @@ -0,0 +1,92 @@ +const router = require("express").Router(); +const fs = require("fs"); + +const excludeIps = new Set(["192.168.1.50", "73.19.173.54"]); + +// Use this function to flatten the Express router stack +function flattenRouterLayers(stack, acc = []) { + for (const layer of stack) { + acc.push(layer); + const h = layer.handle; + if (typeof h === "function") { + if (h.stack && Array.isArray(h.stack)) { + flattenRouterLayers(h.stack, acc); + } else if (h.handle && h.handle.stack && Array.isArray(h.handle.stack)) { + flattenRouterLayers(h.handle.stack, acc); + } + } + } + return acc; +} + +// Collect excludeRoutes from Express router layers +function getExcludeRoutes(router) { + const rootStack = router.stack; + const flat = flattenRouterLayers(rootStack); + const routes = []; + for (const l of flat) { + if (l.route && l.route.path) { + routes.push(l.route.path); + } + } + return routes; +} + +function shouldExclude(ip, url, excludeRoutes) { + if (excludeIps.has(ip)) return true; + + for (const route of excludeRoutes) { + if ( + route.includes(":token") || + route.includes(":year") || + route.includes(":month") || + route.includes(":name") + ) { + const routePrefix = route.split(":")[0]; + if (url.startsWith(routePrefix)) return true; + } else { + if (url === route || url.startsWith(route)) return true; + } + } + return false; +} + +function parseLogLine(line) { + const parts = line.split(" "); + if (parts.length < 1) return null; + const ip = parts[0]; + + const match = line.match(/"([^"]*)"/); + if (!match) return null; + + const request = match[1].split(" "); + if (request.length < 2) return null; + + return { ip, url: request[1] }; +} + +// Route that returns filtered logs as plaintext +router.get("/filtered-logs", (req, res) => { + const excludeRoutes = getExcludeRoutes(req.app._router); + const logPath = "/var/log/nginx/access.log"; + + try { + const input = fs.readFileSync(logPath, "utf8"); + const lines = input.split("\n"); + const filtered = []; + + for (const line of lines) { + if (!line.trim()) continue; + const parsed = parseLogLine(line); + if (!parsed) continue; + + if (!shouldExclude(parsed.ip, parsed.url, excludeRoutes)) { + filtered.push(line); + } + } + + res.type("text/plain").status(200).send(filtered.join("\n")); + } catch { + res.sendStatus(500); + } +}); diff --git a/src/routes/secured/index.js b/src/routes/secured/index.js new file mode 100644 index 0000000..921f8e5 --- /dev/null +++ b/src/routes/secured/index.js @@ -0,0 +1,10 @@ +// src/routes/index.js +const express = require("express"); +const router = express.Router(); + +const logs = require("./logs"); + +router.use(logs); +// router.use(routesList); + +module.exports = router; diff --git a/src/routes/secured/logs.js b/src/routes/secured/logs.js new file mode 100644 index 0000000..06c6e34 --- /dev/null +++ b/src/routes/secured/logs.js @@ -0,0 +1,149 @@ +const express = require("express"); +const router = express.Router(); +const Database = require("better-sqlite3"); +const path = require("path"); +const fs = require("fs"); + +const allowedLevels = ["warn", "error", "info", "debug", "functions", "notice"]; + +const dbPath = path.resolve(__dirname, "../../../data/logs.sqlite3"); + +if (!fs.existsSync(dbPath)) { + fs.closeSync(fs.openSync(dbPath, "w")); +} + +const db = new Database(dbPath, { readonly: true }); + +router.get("/logs", (req, res) => { + res.renderWithBaseContext("pages/logs", { + showSidebar: false, + showFooter: false, + }); +}); + +router.post("/logs", (req, res) => { + const start = process.hrtime.bigint(); + + const log_level = req.query.log_level || "*"; + const date = req.query.date || "*"; + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 50; + const offset = (page - 1) * limit; + + const parseStart = process.hrtime.bigint(); + + if (log_level !== "*" && !allowedLevels.includes(log_level)) { + return res.status(400).json({ error: "Invalid log_level" }); + } + + const conditions = []; + const params = []; + + if (log_level !== "*") { + conditions.push("level = ?"); + params.push(log_level); + } + + if (date !== "*") { + conditions.push("date(timestamp) = ?"); + params.push(date); + } + + const whereClause = conditions.length + ? "WHERE " + conditions.join(" AND ") + : ""; + + const countStart = process.hrtime.bigint(); + + // Count query - simple and fast + const countQuery = `SELECT COUNT(*) as total FROM logs ${whereClause}`; + const totalResult = db.prepare(countQuery).get(...params); + const total = totalResult.total; + + const queryStart = process.hrtime.bigint(); + + // STEP 1: Get just the log records we need (fast!) + const logQuery = ` + SELECT id, timestamp, level + FROM logs + ${whereClause} + ORDER BY timestamp DESC + LIMIT ? OFFSET ? + `; + + try { + const logRows = db.prepare(logQuery).all(...params, limit, offset); + + if (logRows.length === 0) { + return res.json({ + logs: [], + pagination: { page, limit, total, totalPages: 0, hasMore: false }, + }); + } + + // STEP 2: Get metadata only for these specific logs + const logIds = logRows.map((row) => row.id); + const placeholders = logIds.map(() => "?").join(","); + + const metadataQuery = ` + SELECT + m.log_id, + k.key, + m.value + FROM log_metadata m + JOIN keys k ON k.id = m.key_id + WHERE m.log_id IN (${placeholders}) + `; + + const metadataRows = db.prepare(metadataQuery).all(...logIds); + + const mapStart = process.hrtime.bigint(); + + // STEP 3: Build metadata lookup map + const metadataMap = {}; + metadataRows.forEach((row) => { + if (!metadataMap[row.log_id]) { + metadataMap[row.log_id] = {}; + } + try { + metadataMap[row.log_id][row.key] = JSON.parse(row.value); + } catch { + metadataMap[row.log_id][row.key] = row.value; + } + }); + + // STEP 4: Combine logs with their metadata + const logs = logRows.map((row) => ({ + id: row.id, + timestamp: row.timestamp, + level: row.level, + ...(metadataMap[row.id] || {}), + })); + + const end = process.hrtime.bigint(); + + req.log.info("logs route timings", { + totalMs: Number(end - start) / 1e6, + parseMs: Number(parseStart - start) / 1e6, + countMs: Number(queryStart - countStart) / 1e6, + queryMs: Number(mapStart - queryStart) / 1e6, + mapMs: Number(end - mapStart) / 1e6, + }); + + res.json({ + logs, + pagination: { + page, + limit, + total, + totalPages: Math.ceil(total / limit), + hasMore: page < Math.ceil(total / limit), + }, + }); + } catch (error) { + console.error("Query error:", error); + res.status(500).json({ error: "Failed to query logs" }); + } +}); + +module.exports = router; diff --git a/src/routes/secured/routesList.js b/src/routes/secured/routesList.js new file mode 100644 index 0000000..68e4e8a --- /dev/null +++ b/src/routes/secured/routesList.js @@ -0,0 +1,39 @@ +// src/routes/secured/routesList.js +const express = require("express"); +const { getRoutes, refreshRoutes } = require("../../middleware/routesList"); + +const router = express.Router(); + +router.get("/routes", (req, res) => { + try { + const routes = getRoutes(); + res.json({ + count: routes.length, + routes: routes, + }); + } catch (error) { + res.status(500).json({ + error: "Failed to retrieve routes", + message: error.message, + }); + } +}); + +// Optional: endpoint to refresh the route cache +router.post("/routes/refresh", (req, res) => { + try { + const routes = refreshRoutes(); + res.json({ + message: "Routes refreshed", + count: routes.length, + routes: routes, + }); + } catch (error) { + res.status(500).json({ + error: "Failed to refresh routes", + message: error.message, + }); + } +}); + +module.exports = router; diff --git a/src/services/newsletterService.js b/src/services/newsletterService.js index 84ba7e9..be3a651 100644 --- a/src/services/newsletterService.js +++ b/src/services/newsletterService.js @@ -1,57 +1,52 @@ // src/services/newsletterService.js -let writeLock = Promise.resolve(); - const fs = require("fs").promises; const path = require("path"); +const { + FILE_PATH, + EMAIL_REGEX, + ERRORS, +} = require("../constants/newsletterConstants"); -const filePath = path.join(__dirname, "../../data/newsletter-emails.json"); +let writeLock = Promise.resolve(); -// Basic email regex validation function isValidEmail(email) { - return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); + return EMAIL_REGEX.test(email); } async function saveEmail(email) { try { if (!isValidEmail(email)) { - throw new Error("Invalid email format"); + throw new Error(ERRORS.INVALID_EMAIL); } - // Sanitize input: trim whitespace and lowercase const sanitizedEmail = email.trim().toLowerCase(); - // Ensure the directory exists - await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.mkdir(path.dirname(FILE_PATH), { recursive: true }); writeLock = writeLock.then(async () => { let data = []; try { - const file = await fs.readFile(filePath, "utf8"); - // Attempt to parse the file content + const file = await fs.readFile(FILE_PATH, "utf8"); data = JSON.parse(file); } catch (e) { - // If file doesn't exist (ENOENT) or contains invalid JSON (SyntaxError), - // we treat it as an empty array and proceed. - // Other errors should still be re-thrown. if (e.code !== "ENOENT" && !(e instanceof SyntaxError)) { - console.error("Failed to parse newsletter-emails.json:", e); + console.error(ERRORS.PARSE_FAILURE, e); throw e; } - // If ENOENT or SyntaxError, 'data' remains an empty array, which is desired. } if (!data.includes(sanitizedEmail)) { data.push(sanitizedEmail); try { - await fs.writeFile(filePath, JSON.stringify(data, null, 2)); + await fs.writeFile(FILE_PATH, JSON.stringify(data, null, 2)); } catch (err) { - console.error("writeFile failed:", err); + console.error(ERRORS.WRITE_FAILURE, err); throw err; } } }); } catch (err) { - console.error("Failed to save email:", err); + console.error(ERRORS.SAVE_EMAIL_FAILURE, err); throw err; } return await writeLock; diff --git a/src/services/postsMenuService.js b/src/services/postsMenuService.js index 3ccad2d..c9966fe 100644 --- a/src/services/postsMenuService.js +++ b/src/services/postsMenuService.js @@ -1,4 +1,4 @@ -// src/services/postsMenuService.js (refactored) +// src/services/postsMenuService.js const { getAllPosts } = require("../utils/postFileUtils"); const { qualifyLink } = require("../utils/qualifyLinks"); diff --git a/src/services/rssFeedService.js b/src/services/rssFeedService.js index 00b93cd..c999a3a 100644 --- a/src/services/rssFeedService.js +++ b/src/services/rssFeedService.js @@ -1,22 +1,27 @@ // src/services/rssFeedService.js const RSS = require("rss"); const { getAllPosts } = require("../utils/postFileUtils"); +const { + FEED_TITLE, + FEED_DESCRIPTION, + FEED_LANGUAGE, +} = require("../constants/rssConstants"); async function generateRSSFeed(baseDir, siteUrl) { const allPosts = await getAllPosts(baseDir); const feed = new RSS({ - title: "My Blog", - description: "Latest posts from my blog", + title: FEED_TITLE, + description: FEED_DESCRIPTION, feed_url: `${siteUrl}/rss.xml`, site_url: siteUrl, - language: "en", + language: FEED_LANGUAGE, }); for (const post of allPosts) { feed.item({ title: post.title, - description: post.excerpt || "", // optional: add excerpt to post object + description: post.excerpt || "", url: `${siteUrl}${post.url}`, date: post.date, }); diff --git a/src/services/sitemapService.js b/src/services/sitemapService.js index 9d47214..14422cc 100644 --- a/src/services/sitemapService.js +++ b/src/services/sitemapService.js @@ -1,22 +1,27 @@ -// src/services/sitemapService.js (refactored) +// src/services/sitemapService.js const path = require("path"); const fs = require("fs").promises; const { getAllPosts } = require("../utils/postFileUtils"); +const { + STATIC_SITEMAP_PATH, + POSTS_PATH, + DEFAULT_CHANGEFREQ, + DEFAULT_PRIORITY, + BLOG_POST_CHANGEFREQ, + BLOG_POST_PRIORITY, +} = require("../constants/sitemapConstants"); class SitemapService { constructor() { - this.staticSitemapPath = path.resolve( - __dirname, - "../../content/sitemap.json" - ); - this.postsPath = path.join(__dirname, "../../content/posts"); + this.staticSitemapPath = path.resolve(__dirname, STATIC_SITEMAP_PATH); + this.postsPath = path.join(__dirname, POSTS_PATH); } async getStaticPages() { try { const data = await fs.readFile(this.staticSitemapPath, "utf-8"); return JSON.parse(data); - } catch (error) { + } catch { console.warn("Could not load static sitemap.json, using empty array"); return []; } @@ -30,8 +35,8 @@ lastmod: post.date ? new Date(post.date).toISOString().split("T")[0] : null, - changefreq: "monthly", - priority: "0.7", + changefreq: BLOG_POST_CHANGEFREQ, + priority: BLOG_POST_PRIORITY, })); } @@ -41,7 +46,6 @@ this.getBlogPostUrls(), ]); - // Add blog posts as a section in the sitemap const blogSection = { title: "Blog Posts", children: blogUrls.map((url) => ({ @@ -67,8 +71,8 @@ out.push({ loc: entry.loc, lastmod: entry.lastmod, - changefreq: entry.changefreq || "monthly", - priority: entry.priority || "0.5", + changefreq: entry.changefreq || DEFAULT_CHANGEFREQ, + priority: entry.priority || DEFAULT_PRIORITY, }); } if (Array.isArray(entry.children)) { diff --git a/src/utils/baseContext.js b/src/utils/baseContext.js index f2875e5..ca43b5a 100644 --- a/src/utils/baseContext.js +++ b/src/utils/baseContext.js @@ -7,27 +7,36 @@ const navLinks = require(path.join(__dirname, "../../content/navLinks.json")); const filterSecureLinks = require("../utils/filterSecureLinks"); +const getSiteTitle = (owner) => `${owner}'s Software Blog`; + +const POSTS_DIR = path.join(__dirname, "../../content/posts"); +const DEFAULT_CONTEXT = { + showSidebar: true, + showFooter: true, +}; + module.exports = async function getBaseContext( isAuthenticated, overrides = {} ) { const filteredNavLinks = filterSecureLinks(navLinks, isAuthenticated); const qualifiedNavLinks = qualifyNavLinks(filteredNavLinks); - const menu = await getPostsMenu(path.join(__dirname, "../../content/posts")); + const menu = await getPostsMenu(POSTS_DIR); + const siteOwner = process.env.SITE_OWNER; - return Object.assign( - { - siteOwner: process.env.SITE_OWNER, - originCountry: process.env.COUNTRY, - hCaptchaKey: process.env.HCAPTCHA_KEY, - navLinks: qualifiedNavLinks, - years: menu, - formatMonth, - baseUrl, - isAuthenticated, - showSidebar: true, - showFooter: true, - }, - overrides - ); + const context = { + title: getSiteTitle(siteOwner), + siteOwner, + originCountry: process.env.COUNTRY, + hCaptchaKey: process.env.HCAPTCHA_KEY, + navLinks: qualifiedNavLinks, + years: menu, + formatMonth, + baseUrl, + isAuthenticated, + ...DEFAULT_CONTEXT, + ...overrides, + }; + + return context; }; diff --git a/src/utils/formLimiter.js b/src/utils/formLimiter.js index f28e0d9..d413bbc 100644 --- a/src/utils/formLimiter.js +++ b/src/utils/formLimiter.js @@ -1,8 +1,13 @@ const rateLimit = require("express-rate-limit"); +const RATE_LIMIT_WINDOW_MS = 60 * 1000; // 1 minute +const RATE_LIMIT_MAX_REQUESTS = 5; +const RATE_LIMIT_MESSAGE = "Too many requests, please try again later."; + const formLimiter = rateLimit({ - windowMs: 1 * 60 * 1000, // 1 minute - max: 5, // max 5 requests per window per IP - message: "Too many requests, please try again later.", + windowMs: RATE_LIMIT_WINDOW_MS, + max: RATE_LIMIT_MAX_REQUESTS, + message: RATE_LIMIT_MESSAGE, }); + module.exports = formLimiter; diff --git a/src/utils/sendContactMail.js b/src/utils/sendContactMail.js index 6b53e2a..12ebf7c 100644 --- a/src/utils/sendContactMail.js +++ b/src/utils/sendContactMail.js @@ -1,7 +1,12 @@ -// src/utils/sendContactMail.js const transporter = require("./transporter"); -// Basic sanitization and validation functions +const MAIL_DOMAIN = process.env.MAIL_DOMAIN; +const MAIL_USER = process.env.MAIL_USER; +const DEFAULT_SUBJECT = "New Contact Form Submission"; +const MAX_MESSAGE_LENGTH = 2000; + +const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + function sanitizeInput(input) { return String(input) .replace(/[\r\n<>]/g, "") @@ -9,37 +14,32 @@ } function isValidEmail(email) { - return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); + return EMAIL_REGEX.test(email); } function sendContactMail({ name, email, subject, message }) { - const { MAIL_DOMAIN: domain } = process.env; - - // Sanitize inputs const cleanName = sanitizeInput(name); const cleanEmail = sanitizeInput(email); - const cleanSubject = sanitizeInput(subject || "New Contact Form Submission"); + const cleanSubject = sanitizeInput(subject || DEFAULT_SUBJECT); const cleanMessage = sanitizeInput(message); - // Validate email if (!isValidEmail(cleanEmail)) { throw new Error("Invalid email format"); } - const data = { - from: `"Contact Form" `, - to: process.env.MAIL_USER, + if (cleanMessage.length > MAX_MESSAGE_LENGTH) { + throw new Error("Message too long"); + } + + const mailData = { + from: `"Contact Form" `, + to: MAIL_USER, replyTo: `"${cleanName}" <${cleanEmail}>`, subject: cleanSubject, text: cleanMessage, }; - // Optional: limit message length to prevent abuse - if (cleanMessage.length > 2000) { - throw new Error("Message too long"); - } - - return transporter.sendMail(data); + return transporter.sendMail(mailData); } module.exports = sendContactMail; diff --git a/src/utils/sendNewsletterSubscriptionMail.js b/src/utils/sendNewsletterSubscriptionMail.js index c8fae17..9265a01 100644 --- a/src/utils/sendNewsletterSubscriptionMail.js +++ b/src/utils/sendNewsletterSubscriptionMail.js @@ -1,19 +1,26 @@ -// src/utils/sendNewsletterSubscriptionMail.js const transporter = require("./transporter"); -const sendNewsletterSubscriptionMail = async function ({ email }) { - const { MAIL_DOMAIN: domain } = process.env; - const data = { - from: `"Newsletter" `, + +const MAIL_DOMAIN = process.env.MAIL_DOMAIN; +const MAIL_NEWSLETTER = process.env.MAIL_NEWSLETTER; + +const MAIL_SUBJECT = "New Newsletter Subscription"; +const MAIL_FROM = `Newsletter `; +const MAIL_TEXT_TEMPLATE = (email) => + `Please add this email to the newsletter list: ${MAIL_NEWSLETTER}`; + +async function sendNewsletterSubscriptionMail({ email }) { + const mailData = { + from: MAIL_FROM, to: email, - subject: "New Newsletter Subscription", - text: `Please add this email to the newsletter list: ${process.env.MAIL_NEWSLETTER}`, + subject: MAIL_SUBJECT, + text: MAIL_TEXT_TEMPLATE(email), }; + try { - const result = await transporter.sendMail(data); - return result; - } catch (e) { - console.log(e); + return await transporter.sendMail(mailData); + } catch (error) { + console.error(error); } -}; +} module.exports = sendNewsletterSubscriptionMail; diff --git a/src/utils/validateEmail.js b/src/utils/validateEmail.js index 5ae0b4f..249b95c 100644 --- a/src/utils/validateEmail.js +++ b/src/utils/validateEmail.js @@ -1,29 +1,33 @@ const validator = require("validator"); -// Email validation function +const MESSAGES = { + REQUIRED: "Email is required", + TOO_LONG: "Email address is too long", + INVALID: "Please enter a valid email address", +}; + +const MAX_EMAIL_LENGTH = 254; + const validateEmail = (email) => { - if (!email || typeof email !== 'string') { - return { valid: false, message: "Email is required" }; + if (!email || typeof email !== "string") { + return { valid: false, message: MESSAGES.REQUIRED }; } - - // Trim and normalize + email = email.trim().toLowerCase(); - - // Length check - if (email.length > 254) { - return { valid: false, message: "Email address is too long" }; + + if (email.length > MAX_EMAIL_LENGTH) { + return { valid: false, message: MESSAGES.TOO_LONG }; } - - // Basic validation - if (!validator.isEmail(email)) { - return { valid: false, message: "Please enter a valid email address" }; + + if ( + !validator.isEmail(email) || + email.includes("..") || + email.startsWith(".") || + email.endsWith(".") + ) { + return { valid: false, message: MESSAGES.INVALID }; } - - // Additional checks for suspicious patterns - if (email.includes('..') || email.startsWith('.') || email.endsWith('.')) { - return { valid: false, message: "Please enter a valid email address" }; - } - + return { valid: true, email }; }; diff --git a/src/views/layouts/main.handlebars b/src/views/layouts/main.handlebars index 0f3d434..48d2394 100644 --- a/src/views/layouts/main.handlebars +++ b/src/views/layouts/main.handlebars @@ -11,9 +11,15 @@ {{{_sections.styles}}} {{title}} + + + + + {{{_sections.headers}}} + - + {{> headers}}
{{#if showSidebar}} diff --git a/src/views/pages/authRedirect.handlebars b/src/views/pages/authRedirect.handlebars new file mode 100644 index 0000000..23bb188 --- /dev/null +++ b/src/views/pages/authRedirect.handlebars @@ -0,0 +1,5 @@ +
+

Redirecting...

+

Please wait while we redirect you to the authentication service.

+

If you are not redirected automatically, click here.

+
diff --git a/src/views/pages/loading.handlebars b/src/views/pages/loading.handlebars new file mode 100644 index 0000000..a8d7676 --- /dev/null +++ b/src/views/pages/loading.handlebars @@ -0,0 +1,16 @@ +{{#section "headers"}} + +{{/section}} +{{#section "styles"}} + +{{/section}} +{{#section " scripts"}} + +{{/section}} +
+
+
+

Loading...

+
+
diff --git a/src/views/pages/redirect.handlebars b/src/views/pages/redirect.handlebars index 23bb188..1a9563d 100644 --- a/src/views/pages/redirect.handlebars +++ b/src/views/pages/redirect.handlebars @@ -1,5 +1,21 @@ -
-

Redirecting...

-

Please wait while we redirect you to the authentication service.

-

If you are not redirected automatically, click here.

+{{#section "headers"}} + + +{{/section}} +{{#section "styles"}} + +{{/section}} +{{#section " scripts"}} + +{{/section}} +
+
+
+

Redirecting...

+

Taking you to your destination.

+

+ Go Now +

+
diff --git a/src/views/partials/favicon.handlebars b/src/views/partials/favicon.handlebars new file mode 100644 index 0000000..06998b5 --- /dev/null +++ b/src/views/partials/favicon.handlebars @@ -0,0 +1,12 @@ +
+
+
+
+
+
+
+
+
+
+ λ +
diff --git a/src/views/partials/headers.handlebars b/src/views/partials/headers.handlebars index 0b69c6b..defba3f 100644 --- a/src/views/partials/headers.handlebars +++ b/src/views/partials/headers.handlebars @@ -1,10 +1,18 @@ -