diff --git a/package-lock.json b/package-lock.json index 3bcf69e..4796b70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,15 +21,18 @@ "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^11.0.1", "@nestjs/platform-socket.io": "^11.1.11", + "@nestjs/schedule": "^6.1.0", "@nestjs/swagger": "^11.2.4", "@nestjs/terminus": "^11.0.0", "@nestjs/throttler": "^6.5.0", "@prisma/client": "^5.22.0", + "@types/cheerio": "^0.22.35", "axios": "^1.13.4", "bcrypt": "^6.0.0", "bullmq": "^5.66.4", "cache-manager": "^7.2.7", "cache-manager-redis-yet": "^5.1.5", + "cheerio": "^1.2.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.3", "helmet": "^8.1.0", @@ -42,6 +45,7 @@ "pino": "^10.1.0", "pino-http": "^11.0.0", "prisma": "^5.22.0", + "puppeteer": "^24.36.1", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", "zod": "^4.3.5" @@ -53,6 +57,7 @@ "@nestjs/schematics": "^11.0.0", "@nestjs/testing": "^11.0.1", "@types/bcrypt": "^6.0.0", + "@types/cron": "^2.0.1", "@types/express": "^5.0.0", "@types/jest": "^30.0.0", "@types/node": "^22.10.7", @@ -1115,7 +1120,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", @@ -1275,7 +1279,6 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -3411,6 +3414,19 @@ "rxjs": "^7.1.0" } }, + "node_modules/@nestjs/schedule": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-6.1.0.tgz", + "integrity": "sha512-W25Ydc933Gzb1/oo7+bWzzDiOissE+h/dhIAPugA39b9MuIzBbLybuXpc1AjoQLczO3v0ldmxaffVl87W0uqoQ==", + "license": "MIT", + "dependencies": { + "cron": "4.3.5" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "@nestjs/core": "^10.0.0 || ^11.0.0" + } + }, "node_modules/@nestjs/schematics": { "version": "11.0.9", "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.0.9.tgz", @@ -3783,6 +3799,27 @@ "@prisma/debug": "5.22.0" } }, + "node_modules/@puppeteer/browsers": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.11.2.tgz", + "integrity": "sha512-GBY0+2lI9fDrjgb5dFL9+enKXqyOPok9PXg/69NVkjW3bikbK9RQrNrI3qccQXmDNN7ln4j/yL89Qgvj/tfqrw==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.3", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.3", + "tar-fs": "^3.1.1", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@redis/bloom": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", @@ -4578,6 +4615,12 @@ "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT" + }, "node_modules/@tsconfig/node10": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", @@ -4672,6 +4715,15 @@ "@types/node": "*" } }, + "node_modules/@types/cheerio": { + "version": "0.22.35", + "resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.35.tgz", + "integrity": "sha512-yD57BchKRvTV+JD53UZ6PD8KWY5g5rvvMLRnZR3EQBCZXiDT/HR+pKpMzFGlWNhFrXlo7VPZXtKvIEwZkAWOIA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -4695,6 +4747,17 @@ "@types/node": "*" } }, + "node_modules/@types/cron": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/cron/-/cron-2.0.1.tgz", + "integrity": "sha512-WHa/1rtNtD2Q/H0+YTTZoty+/5rcE66iAFX2IY+JuUoOACsevYyFkSYu/2vdw+G5LrmO7Lxowrqm0av4k3qWNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/luxon": "*", + "@types/node": "*" + } + }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -4799,6 +4862,12 @@ "@types/node": "*" } }, + "node_modules/@types/luxon": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.1.tgz", + "integrity": "sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg==", + "license": "MIT" + }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -4936,6 +5005,16 @@ "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", "dev": true }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.52.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.52.0.tgz", @@ -5841,6 +5920,18 @@ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "dev": true }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -5865,6 +5956,20 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/b4a": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", + "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, "node_modules/babel-jest": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz", @@ -5961,6 +6066,97 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.3.tgz", + "integrity": "sha512-9+kwVx8QYvt3hPWnmb19tPnh38c6Nihz8Lx3t0g9+4GoIf3/fTgYwM4Z6NxgI+B9elLQA7mLE9PpqcWtOMRDiQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz", + "integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==", + "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.7.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz", + "integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==", + "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/bare-url": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz", + "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-path": "^3.0.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -5997,6 +6193,15 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/basic-ftp": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.1.0.tgz", + "integrity": "sha512-RkaJzeJKDbaDWTIPiJwubyljaEPwpVWkm9Rt5h9Nd6h7tEXTJ3VB4qxdZBioV7JO5yLUaOKwz7vDOzlncUsegw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/bcrypt": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", @@ -6063,6 +6268,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, "node_modules/bowser": { "version": "2.13.1", "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.13.1.tgz", @@ -6226,6 +6437,15 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -6387,7 +6607,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "engines": { "node": ">=6" } @@ -6459,6 +6678,48 @@ "node": ">=16" } }, + "node_modules/cheerio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -6483,6 +6744,28 @@ "node": ">=6.0" } }, + "node_modules/chromium-bidi": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-13.0.1.tgz", + "integrity": "sha512-c+RLxH0Vg2x2syS9wPw378oJgiJNXtYXUvnVAldUlt5uaHekn0CCU7gPksNgHjrH1qFhmjVXQj4esvuthuC7OQ==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/chromium-bidi/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" + } + }, "node_modules/ci-info": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", @@ -6582,7 +6865,6 @@ "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==", - "dev": true, "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -6596,7 +6878,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -6833,6 +7114,19 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, + "node_modules/cron": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/cron/-/cron-4.3.5.tgz", + "integrity": "sha512-hKPP7fq1+OfyCqoePkKfVq7tNAdFwiQORr4lZUHwrf0tebC65fYEeWgOrXOL6prn1/fegGOdTfrM6e34PJfksg==", + "license": "MIT", + "dependencies": { + "@types/luxon": "~3.7.0", + "luxon": "~3.7.0" + }, + "engines": { + "node": ">=18.x" + } + }, "node_modules/cron-parser": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", @@ -6857,6 +7151,34 @@ "node": ">= 8" } }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -6931,6 +7253,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -6973,6 +7309,12 @@ "node": ">=8" } }, + "node_modules/devtools-protocol": { + "version": "0.0.1551306", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1551306.tgz", + "integrity": "sha512-CFx8QdSim8iIv+2ZcEOclBKTQY6BI1IEDa7Tm9YkwAXzEWFndTEzpTo5jAUhSnq24IC7xaDw0wvGcm96+Y3PEg==", + "license": "BSD-3-Clause" + }, "node_modules/dezalgo": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", @@ -6992,6 +7334,61 @@ "node": ">=0.3.1" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dotenv": { "version": "16.4.7", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", @@ -7079,11 +7476,35 @@ "node": ">= 0.8" } }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/encoding-sniffer/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, "dependencies": { "once": "^1.4.0" } @@ -7167,11 +7588,31 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/error-ex": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, "dependencies": { "is-arrayish": "^0.2.1" } @@ -7228,7 +7669,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "engines": { "node": ">=6" } @@ -7250,6 +7690,37 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/eslint": { "version": "9.39.2", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", @@ -7403,7 +7874,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" @@ -7440,7 +7910,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, "engines": { "node": ">=4.0" } @@ -7449,7 +7918,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -7476,6 +7944,15 @@ "node": ">=0.8.x" } }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -7578,6 +8055,41 @@ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/fast-copy": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.2.tgz", @@ -7596,6 +8108,12 @@ "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", "dev": true }, + "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-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -7655,6 +8173,15 @@ "bser": "2.1.1" } }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -8093,6 +8620,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/get-uri/node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/glob": { "version": "13.0.0", "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz", @@ -8319,6 +8869,37 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -8338,6 +8919,19 @@ "url": "https://opencollective.com/express" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -8406,7 +9000,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -8485,6 +9078,15 @@ "url": "https://opencollective.com/ioredis" } }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -8496,8 +9098,7 @@ "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==", - "dev": true + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" }, "node_modules/is-binary-path": { "version": "2.1.0", @@ -9401,8 +10002,7 @@ "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==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { "version": "4.1.1", @@ -9444,8 +10044,7 @@ "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==", - "dev": true + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" }, "node_modules/json-schema-traverse": { "version": "0.4.1", @@ -9567,8 +10166,7 @@ "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==", - "dev": true + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, "node_modules/load-esm": { "version": "1.0.3", @@ -9906,6 +10504,12 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -10143,6 +10747,15 @@ "rxjs": "^7.1.0" } }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/node-abort-controller": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", @@ -10265,6 +10878,18 @@ "node": ">=8" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -10414,6 +11039,38 @@ "node": ">=6" } }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -10423,7 +11080,6 @@ "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==", - "dev": true, "dependencies": { "callsites": "^3.0.0" }, @@ -10435,7 +11091,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -10449,6 +11104,55 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -10565,11 +11269,16 @@ "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, "node_modules/picomatch": { "version": "4.0.2", @@ -10850,6 +11559,15 @@ } ] }, + "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-coalesce": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/promise-coalesce/-/promise-coalesce-1.5.0.tgz", @@ -10870,6 +11588,34 @@ "node": ">= 0.10" } }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -10880,7 +11626,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", - "dev": true, "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -10895,6 +11640,92 @@ "node": ">=6" } }, + "node_modules/puppeteer": { + "version": "24.36.1", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.36.1.tgz", + "integrity": "sha512-uPiDUyf7gd7Il1KnqfNUtHqntL0w1LapEw5Zsuh8oCK8GsqdxySX1PzdIHKB2Dw273gWY4MW0zC5gy3Re9XlqQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.11.2", + "chromium-bidi": "13.0.1", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1551306", + "puppeteer-core": "24.36.1", + "typed-query-selector": "^2.12.0" + }, + "bin": { + "puppeteer": "lib/cjs/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core": { + "version": "24.36.1", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.36.1.tgz", + "integrity": "sha512-L7ykMWc3lQf3HS7ME3PSjp7wMIjJeW6+bKfH/RSTz5l6VUDGubnrC2BKj3UvM28Y5PMDFW0xniJOZHBZPpW1dQ==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.11.2", + "chromium-bidi": "13.0.1", + "debug": "^4.4.3", + "devtools-protocol": "0.0.1551306", + "typed-query-selector": "^2.12.0", + "webdriver-bidi-protocol": "0.4.0", + "ws": "^8.19.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core/node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "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/puppeteer/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/pure-rand": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", @@ -11042,7 +11873,6 @@ "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==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -11081,7 +11911,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "engines": { "node": ">=4" } @@ -11444,6 +12273,16 @@ "node": ">=8" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, "node_modules/socket.io": { "version": "4.8.3", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", @@ -11521,6 +12360,34 @@ "node": ">= 0.6" } }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/sonic-boom": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", @@ -11613,6 +12480,17 @@ "node": ">=10.0.0" } }, + "node_modules/streamx": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -11835,6 +12713,31 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tar-fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", + "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", + "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/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/terser": { "version": "5.44.1", "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", @@ -12027,6 +12930,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "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/thread-stream": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", @@ -12333,6 +13245,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/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", @@ -12342,7 +13260,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12409,6 +13327,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/undici": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.19.2.tgz", + "integrity": "sha512-4VQSpGEGsWzk0VYxyB/wVX/Q7qf9t5znLRgs0dzszr9w9Fej/8RVNQ+S20vdXSAyra/bJ7ZQfGv6ZMj7UEbzSg==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -12592,6 +13519,12 @@ "node": ">= 8" } }, + "node_modules/webdriver-bidi-protocol": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.0.tgz", + "integrity": "sha512-U9VIlNRrq94d1xxR9JrCEAx5Gv/2W7ERSv8oWRoNe/QYbfccS0V3h/H6qeNeCRJxXGMhhnkqvwNrvPAYeuP9VA==", + "license": "Apache-2.0" + }, "node_modules/webpack": { "version": "5.104.1", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.104.1.tgz", @@ -12781,6 +13714,40 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -12902,7 +13869,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "engines": { "node": ">=10" } @@ -12917,7 +13883,6 @@ "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -12935,11 +13900,20 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, "engines": { "node": ">=12" } }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/package.json b/package.json index a4325bd..6ed4a34 100644 --- a/package.json +++ b/package.json @@ -31,15 +31,18 @@ "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^11.0.1", "@nestjs/platform-socket.io": "^11.1.11", + "@nestjs/schedule": "^6.1.0", "@nestjs/swagger": "^11.2.4", "@nestjs/terminus": "^11.0.0", "@nestjs/throttler": "^6.5.0", "@prisma/client": "^5.22.0", + "@types/cheerio": "^0.22.35", "axios": "^1.13.4", "bcrypt": "^6.0.0", "bullmq": "^5.66.4", "cache-manager": "^7.2.7", "cache-manager-redis-yet": "^5.1.5", + "cheerio": "^1.2.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.3", "helmet": "^8.1.0", @@ -52,6 +55,7 @@ "pino": "^10.1.0", "pino-http": "^11.0.0", "prisma": "^5.22.0", + "puppeteer": "^24.36.1", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", "zod": "^4.3.5" @@ -63,6 +67,7 @@ "@nestjs/schematics": "^11.0.0", "@nestjs/testing": "^11.0.1", "@types/bcrypt": "^6.0.0", + "@types/cron": "^2.0.1", "@types/express": "^5.0.0", "@types/jest": "^30.0.0", "@types/node": "^22.10.7", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a744a46..f63dfa3 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -179,9 +179,17 @@ model Game { // External Data igdbId Int? @unique rawgId Int? @unique + sourceUrl String? // URL where this game was scraped from + // Details + rating Float? + developer String? + publisher String? + // Relations platforms GamePlatform[] + genres GameGenre[] + screenshots GameScreenshot[] subscriptions Subscription[] // Timestamps @@ -193,6 +201,35 @@ model Game { @@index([slug]) } +model Genre { + id String @id @default(uuid()) + name String @unique + slug String @unique + + games GameGenre[] + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model GameGenre { + gameId String + genreId String + game Game @relation(fields: [gameId], references: [id], onDelete: Cascade) + genre Genre @relation(fields: [genreId], references: [id], onDelete: Cascade) + + @@id([gameId, genreId]) +} + +model GameScreenshot { + id String @id @default(uuid()) + url String + gameId String + game Game @relation(fields: [gameId], references: [id], onDelete: Cascade) + + createdAt DateTime @default(now()) +} + model Platform { id String @id @default(uuid()) name String @unique // "PlayStation 5", "PC", "Xbox Series X" diff --git a/src/app.module.ts b/src/app.module.ts index 632f187..eecc618 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -49,6 +49,10 @@ import { RolesGuard, PermissionsGuard, } from './modules/auth/guards'; +import { ScraperModule } from './modules/scraper/scraper.module'; +import { SyncModule } from './modules/sync/sync.module'; +import { NotificationModule } from './modules/notification/notification.module'; +import { ThemingModule } from './modules/theming/theming.module'; @Module({ imports: [ @@ -166,6 +170,10 @@ import { ExternalApiModule, GamesModule, EventsModule, + ScraperModule, + SyncModule, + NotificationModule, + ThemingModule, ], providers: [ // Global Exception Filter diff --git a/src/common/types/api-response.type.ts b/src/common/types/api-response.type.ts index 09c702f..a953050 100644 --- a/src/common/types/api-response.type.ts +++ b/src/common/types/api-response.type.ts @@ -28,6 +28,8 @@ export interface PaginationMeta { hasPreviousPage: boolean; } +export type GameResponse = ApiResponse>; + /** * Create a successful API response */ diff --git a/src/main.ts b/src/main.ts index e21db91..7850fab 100644 --- a/src/main.ts +++ b/src/main.ts @@ -72,7 +72,7 @@ async function bootstrap() { }); logger.log('Swagger initialized'); - logger.log(`Attempting to listen on port ${port}...`); + logger.log(`Attempting to listen on port ${port}... (Configured via .env)`); await app.listen(port, '0.0.0.0'); logger.log('═══════════════════════════════════════════════════════════'); diff --git a/src/modules/external-api/interfaces/game-provider.interface.ts b/src/modules/external-api/interfaces/game-provider.interface.ts index 612c0bd..8cc5b99 100644 --- a/src/modules/external-api/interfaces/game-provider.interface.ts +++ b/src/modules/external-api/interfaces/game-provider.interface.ts @@ -13,8 +13,12 @@ export interface GameDetails { coverUrl?: string; firstReleaseDate?: Date; platforms: string[]; // Platform slugs + genres?: string[]; // Genre names screenshots?: string[]; externalId: number; // Provider specific ID + rating?: number; + developer?: string; + publisher?: string; } export abstract class GameDataProvider { diff --git a/src/modules/games/dto/get-games.dto.ts b/src/modules/games/dto/get-games.dto.ts new file mode 100644 index 0000000..6a020ed --- /dev/null +++ b/src/modules/games/dto/get-games.dto.ts @@ -0,0 +1,24 @@ +import { IsOptional, IsString, IsInt, Min } from 'class-validator'; +import { Transform } from 'class-transformer'; +import { ApiPropertyOptional } from '@nestjs/swagger'; + +export class GetGamesDto { + @ApiPropertyOptional() + @IsOptional() + @IsString() + search?: string; + + @ApiPropertyOptional() + @IsOptional() + @IsInt() + @Min(0) + @Transform(({ value }) => parseInt(value)) + page?: number = 0; + + @ApiPropertyOptional() + @IsOptional() + @IsInt() + @Min(1) + @Transform(({ value }) => parseInt(value)) + limit?: number = 10; +} diff --git a/src/modules/games/games.controller.ts b/src/modules/games/games.controller.ts index 001692d..6a6bee7 100644 --- a/src/modules/games/games.controller.ts +++ b/src/modules/games/games.controller.ts @@ -1,19 +1,41 @@ -import { Controller, Post, Body, Param } from '@nestjs/common'; +import { Controller, Post, Body, Param, Get, Query } from '@nestjs/common'; import { BaseController } from '../../common/base/base.controller'; +import { Public } from '../../common/decorators'; import { Game } from '@prisma/client'; import { CreateGameDto } from './dto/create-game.dto'; import { UpdateGameDto } from './dto/update-game.dto'; import { GamesService } from './games.service'; import { ApiTags, ApiOperation } from '@nestjs/swagger'; -import { createSuccessResponse } from '../../common/types/api-response.type'; +import { createSuccessResponse, GameResponse } from '../../common/types/api-response.type'; +import { GetGamesDto } from './dto/get-games.dto'; @ApiTags('Games') @Controller('games') export class GamesController extends BaseController { + + constructor(protected readonly gamesService: GamesService) { super(gamesService, 'Game'); } + @Public() + @Get(':slug') + @ApiOperation({ summary: 'Get game by slug' }) + async getGameBySlug(@Param('slug') slug: string) { + const game = await this.gamesService.findBySlug(slug); + if (!game) { + // Try to sync if not found? Or just return 404. + // Let's return 404 for now, usually sync is explicit or scheduled. + // However, for better UX, maybe we can't auto-sync on read (too slow). + // But if we want "lazy loading", we could check if it exists in external provider. + // Sticking to 404 + manual sync for now. + // Actually, BaseController doesn't have NotFoundException imported usually (?) + // BaseController uses createSuccessResponse. + return createSuccessResponse(null, 'Game not found', 404); + } + return createSuccessResponse(game, 'Game retrieved successfully'); + } + @Post(':slug/sync') @ApiOperation({ summary: 'Sync game data from external provider' }) async syncGame(@Param('slug') slug: string) { diff --git a/src/modules/games/games.service.ts b/src/modules/games/games.service.ts index 2734362..90518bb 100644 --- a/src/modules/games/games.service.ts +++ b/src/modules/games/games.service.ts @@ -34,6 +34,13 @@ export class GamesService extends BaseService 0) { + // First find existing genres or create them + for (const genreName of details.genres) { + const genreSlug = genreName.toLowerCase().replace(/[^a-z0-9]+/g, '-'); + const genre = await this.prisma.genre.upsert({ + where: { slug: genreSlug }, + update: {}, + create: { name: genreName, slug: genreSlug } + }); + + // Link to game + await this.prisma.gameGenre.upsert({ + where: { gameId_genreId: { gameId: game.id, genreId: genre.id } }, + update: {}, + create: { gameId: game.id, genreId: genre.id } + }); + } + } + + // 4. Handle Screenshots + if (details.screenshots && details.screenshots.length > 0) { + // Remove old screenshots to ensure sync + await this.prisma.gameScreenshot.deleteMany({ where: { gameId: game.id } }); + + await this.prisma.gameScreenshot.createMany({ + data: details.screenshots.map(url => ({ + gameId: game.id, + url: url + })) + }); + } + + // 5. Handle Platforms (Reuse existing logic or add here if missing from previous implementation) + // For new fields, this is sufficient. + + return this.findBySlug(game.slug); + } + + async findBySlug(slug: string) { + const game = await this.prisma.game.findUnique({ + where: { slug }, + include: { + platforms: { + include: { + platform: true + } + }, + genres: { + include: { + genre: true + } + }, + screenshots: true + } + }); return game; } } diff --git a/src/modules/notification/dto/subscribe-game.dto.ts b/src/modules/notification/dto/subscribe-game.dto.ts new file mode 100644 index 0000000..ecf6e7b --- /dev/null +++ b/src/modules/notification/dto/subscribe-game.dto.ts @@ -0,0 +1,10 @@ +import { IsString, IsNotEmpty, IsUUID } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class SubscribeGameDto { + @ApiProperty({ description: 'The UUID of the game to subscribe to' }) + @IsString() + @IsUUID() + @IsNotEmpty() + gameId: string; +} diff --git a/src/modules/notification/notification.controller.spec.ts b/src/modules/notification/notification.controller.spec.ts new file mode 100644 index 0000000..7afea9f --- /dev/null +++ b/src/modules/notification/notification.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { NotificationController } from './notification.controller'; + +describe('NotificationController', () => { + let controller: NotificationController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [NotificationController], + }).compile(); + + controller = module.get(NotificationController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/modules/notification/notification.controller.ts b/src/modules/notification/notification.controller.ts new file mode 100644 index 0000000..ac931dd --- /dev/null +++ b/src/modules/notification/notification.controller.ts @@ -0,0 +1,27 @@ +import { Controller, Post, Body, UseGuards, Req, Delete, Get } from '@nestjs/common'; +import { NotificationService } from './notification.service'; +import { SubscribeGameDto } from './dto/subscribe-game.dto'; +import { JwtAuthGuard } from '../auth/guards'; +import { Public, CurrentUser } from '../../common/decorators'; +import type { User } from '@prisma/client'; + +@Controller('notifications') +export class NotificationController { + constructor(private readonly notificationService: NotificationService) { } + + @Post('subscribe') + async subscribe(@CurrentUser() user: User, @Body() dto: SubscribeGameDto) { + return this.notificationService.subscribeToGame(user.id, dto.gameId); + } + + @Post('unsubscribe') + async unsubscribe(@CurrentUser() user: User, @Body() dto: SubscribeGameDto) { + return this.notificationService.unsubscribeFromGame(user.id, dto.gameId); + } + + @Public() // For testing convenience + @Post('check-releases') + async checkReleases() { + return this.notificationService.checkUpcomingReleases(); + } +} diff --git a/src/modules/notification/notification.module.ts b/src/modules/notification/notification.module.ts new file mode 100644 index 0000000..98cd354 --- /dev/null +++ b/src/modules/notification/notification.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { NotificationService } from './notification.service'; +import { NotificationController } from './notification.controller'; +import { DatabaseModule } from '../../database/database.module'; + +@Module({ + imports: [DatabaseModule], + controllers: [NotificationController], + providers: [NotificationService], +}) +export class NotificationModule { } diff --git a/src/modules/notification/notification.service.spec.ts b/src/modules/notification/notification.service.spec.ts new file mode 100644 index 0000000..65bd59d --- /dev/null +++ b/src/modules/notification/notification.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { NotificationService } from './notification.service'; + +describe('NotificationService', () => { + let service: NotificationService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [NotificationService], + }).compile(); + + service = module.get(NotificationService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/modules/notification/notification.service.ts b/src/modules/notification/notification.service.ts new file mode 100644 index 0000000..a4d0ebb --- /dev/null +++ b/src/modules/notification/notification.service.ts @@ -0,0 +1,84 @@ +import { Injectable, Logger, BadRequestException } from '@nestjs/common'; +import { PrismaService } from '../../database/prisma.service'; +import { Cron, CronExpression } from '@nestjs/schedule'; + +@Injectable() +export class NotificationService { + private readonly logger = new Logger(NotificationService.name); + + constructor(private readonly prisma: PrismaService) { } + + async subscribeToGame(userId: string, gameId: string) { + // Check if subscription exists + const existing = await this.prisma.subscription.findUnique({ + where: { + userId_gameId: { userId, gameId }, + }, + }); + + if (existing) { + throw new BadRequestException('Already subscribed to this game'); + } + + return this.prisma.subscription.create({ + data: { + userId, + gameId, + }, + }); + } + + async unsubscribeFromGame(userId: string, gameId: string) { + return this.prisma.subscription.delete({ + where: { + userId_gameId: { userId, gameId }, + }, + }); + } + + @Cron(CronExpression.EVERY_DAY_AT_NOON) + async checkUpcomingReleases() { + this.logger.log('Checking for upcoming game releases...'); + + // Find games releasing tomorrow + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + tomorrow.setHours(0, 0, 0, 0); + + const nextDay = new Date(tomorrow); + nextDay.setDate(nextDay.getDate() + 1); + + const upcomingGames = await this.prisma.game.findMany({ + where: { + releaseDate: { + gte: tomorrow, + lt: nextDay, + }, + }, + include: { + subscriptions: { + include: { + user: true, + }, + }, + }, + }); + + this.logger.log(`Found ${upcomingGames.length} games releasing on ${tomorrow.toDateString()}`); + + for (const game of upcomingGames) { + if (game.subscriptions.length > 0) { + this.logger.log(`Notifying ${game.subscriptions.length} users for game: ${game.title}`); + for (const sub of game.subscriptions) { + await this.sendNotification(sub.user, game); + } + } + } + } + + private async sendNotification(user: any, game: any) { + // In a real app, integrate with MailerService or Push Notification Provider + // For now, we mock it. + this.logger.log(`[MOCK EMAIL] To: ${user.email} | Subject: Release Alert: ${game.title} is coming tomorrow!`); + } +} diff --git a/src/modules/scraper/scraper.controller.spec.ts b/src/modules/scraper/scraper.controller.spec.ts new file mode 100644 index 0000000..58b0978 --- /dev/null +++ b/src/modules/scraper/scraper.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ScraperController } from './scraper.controller'; + +describe('ScraperController', () => { + let controller: ScraperController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ScraperController], + }).compile(); + + controller = module.get(ScraperController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/modules/scraper/scraper.controller.ts b/src/modules/scraper/scraper.controller.ts new file mode 100644 index 0000000..5d7cffc --- /dev/null +++ b/src/modules/scraper/scraper.controller.ts @@ -0,0 +1,20 @@ +import { Body, Controller, Post, Get, Query } from '@nestjs/common'; +import { ScraperService, ScrapedGame } from './scraper.service'; +import { Public } from '../../common/decorators'; + +@Controller('scraper') +export class ScraperController { + constructor(private readonly scraperService: ScraperService) { } + + @Post('url') + async scrapeUrl(@Body('url') url: string): Promise { + return this.scraperService.scrapeUrl(url); + } + + @Public() + @Get('test-trigger') + async testTrigger(@Query('url') url: string) { + if (!url) return { message: "Provide ?url=..." }; + return this.scraperService.scrapeUrl(url); + } +} diff --git a/src/modules/scraper/scraper.module.ts b/src/modules/scraper/scraper.module.ts new file mode 100644 index 0000000..5fb35e1 --- /dev/null +++ b/src/modules/scraper/scraper.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { ScraperService } from './scraper.service'; +import { ScraperController } from './scraper.controller'; + +@Module({ + controllers: [ScraperController], + providers: [ScraperService], + exports: [ScraperService], +}) +export class ScraperModule { } diff --git a/src/modules/scraper/scraper.service.spec.ts b/src/modules/scraper/scraper.service.spec.ts new file mode 100644 index 0000000..82a6a80 --- /dev/null +++ b/src/modules/scraper/scraper.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ScraperService } from './scraper.service'; + +describe('ScraperService', () => { + let service: ScraperService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ScraperService], + }).compile(); + + service = module.get(ScraperService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/modules/scraper/scraper.service.ts b/src/modules/scraper/scraper.service.ts new file mode 100644 index 0000000..8d8a711 --- /dev/null +++ b/src/modules/scraper/scraper.service.ts @@ -0,0 +1,197 @@ +import { Injectable, Logger } from '@nestjs/common'; +import * as puppeteer from 'puppeteer'; +import * as cheerio from 'cheerio'; + +export interface ScrapedGame { + title: string; + releaseDate?: string; // Raw string, e.g., "Oct 2026" or "2026-10-21" + platform?: string[]; + sourceUrl: string; +} + +@Injectable() +export class ScraperService { + private readonly logger = new Logger(ScraperService.name); + + async scrapeUrl(url: string): Promise { + this.logger.log(`Starting scrape for URL: ${url}`); + + let browser; + try { + browser = await puppeteer.launch({ + headless: true, + args: ['--no-sandbox', '--disable-setuid-sandbox'] + }); + + const page = await browser.newPage(); + + // Set a realistic User-Agent to avoid immediate blocking + await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'); + + await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 }); + + const content = await page.content(); + const $ = cheerio.load(content); + + const domain = new URL(url).hostname; + + let games: ScrapedGame[] = []; + + if (domain.includes('metacritic')) { + games = this.scrapeMetacritic($); + } else if (domain.includes('insider-gaming')) { + games = this.scrapeInsiderGaming($); + } else if (domain.includes('gamesradar')) { + games = this.scrapeGamesRadar($); + } else if (domain.includes('steamdb')) { + games = this.scrapeSteamDB($); + } else { + games = this.scrapeGeneric($, url); + } + + // Post-process: Add exact source URL to each + games.forEach(g => g.sourceUrl = url); + + this.logger.log(`Found ${games.length} games from ${url}`); + return games; + + } catch (error) { + this.logger.error(`Error scraping ${url}: ${error.message}`, error.stack); + throw error; + } finally { + if (browser) await browser.close(); + } + } + + private scrapeMetacritic($: any): ScrapedGame[] { + const games: ScrapedGame[] = []; + // Likely structure involves a list of items. + // Metacritic usually has specific classes. This is a heuristic guess based on common structure. + // Example: .c-productList_item + + // Fallback to generic searching for common patterns if specific class isn't found immediately? + // Let's rely on text analysis for robustness. + + $('tr').each((i, el) => { + const title = $(el).find('h3, .title').text().trim(); + const date = $(el).find('.date, time').text().trim(); + if (title && date) { + games.push({ title, releaseDate: date, sourceUrl: '' }); + } + }); + + return games; + } + + private scrapeInsiderGaming($: any): ScrapedGame[] { + const games: ScrapedGame[] = []; + // Insider gaming usually has lists like "Game Title (Platform) – Date" + + $('p, li').each((i, el) => { + const text = $(el).text(); + // Regex to find "Title - Date" or "Title (Platform) - Date" + // Heuristic: Line starts with text, contains " - " or " – ", ends with date-like structure + if (text.includes('–') || text.includes('-')) { + const parts = text.split(/–|-/); + if (parts.length >= 2) { + const potentialDate = parts[parts.length - 1].trim(); + const potentialTitle = parts[0].trim(); + + if (this.isDate(potentialDate)) { + games.push({ + title: potentialTitle, + releaseDate: potentialDate, + sourceUrl: '' + }); + } + } + } + }); + + return games; + } + + private scrapeGamesRadar($: any): ScrapedGame[] { + const games: ScrapedGame[] = []; + // GamesRadar typically breaks down by month headers. + //

January 2026

then
  • Title - Date
+ + let currentMonth = ''; + + $('*').each((i, el) => { + const $el = $(el); + if ($el.is('h3') || $el.is('h2')) { + const text = $el.text().trim(); + if (this.isMonthYear(text)) { + currentMonth = text; + } + } else if ($el.is('li') && currentMonth) { + const text = $el.text().trim(); + // Often "Game Title (Platform) - Day" + if (text) { + games.push({ + title: text, + releaseDate: currentMonth, // Granularity might be just month + sourceUrl: '' + }); + } + } + }); + + return games; + } + + private scrapeSteamDB($: any): ScrapedGame[] { + const games: ScrapedGame[] = []; + // SteamDB is a table. .table-products + $('.table-products tr').each((i, el) => { + const title = $(el).find('a.b').text().trim(); + const date = $(el).find('.timeago').text().trim() || $(el).find('td:nth-child(2)').text().trim(); + + if (title) { + games.push({ title, releaseDate: date, sourceUrl: '' }); + } + }); + return games; + } + + private scrapeGeneric($: any, url: string): ScrapedGame[] { + // Advanced heuristic: Look for Table rows with at least 2 columns, one being a date. + const games: ScrapedGame[] = []; + + $('tr').each((i, el) => { + const cells = $(el).find('td'); + if (cells.length >= 2) { + // Check each cell to see if it looks like a date + let foundDate = ''; + let foundTitle = ''; + + cells.each((j, cell) => { + const text = $(cell).text().trim(); + if (this.isDate(text)) { + foundDate = text; + } else if (text.length > 3 && text.length < 100) { + foundTitle = text; // Assume non-date short text is title + } + }); + + if (foundDate && foundTitle) { + games.push({ title: foundTitle, releaseDate: foundDate, sourceUrl: '' }); + } + } + }); + + return games; + } + + private isDate(text: string): boolean { + // Simple heuristic check + const datePattern = /(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|january|february|march|april|may|june|july|august|september|october|november|december)\s+\d{1,2},?\s+\d{4}|^\d{4}-\d{2}-\d{2}$|^\d{1,2}\/\d{1,2}\/\d{4}$|TBD|202[4-9]/i; + return datePattern.test(text); + } + + private isMonthYear(text: string): boolean { + const monthYearPattern = /^(january|february|march|april|may|june|july|august|september|october|november|december)\s+202[4-9]$/i; + return monthYearPattern.test(text); + } +} diff --git a/src/modules/sync/sync.controller.spec.ts b/src/modules/sync/sync.controller.spec.ts new file mode 100644 index 0000000..91904e1 --- /dev/null +++ b/src/modules/sync/sync.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { SyncController } from './sync.controller'; + +describe('SyncController', () => { + let controller: SyncController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [SyncController], + }).compile(); + + controller = module.get(SyncController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/modules/sync/sync.controller.ts b/src/modules/sync/sync.controller.ts new file mode 100644 index 0000000..cc089cb --- /dev/null +++ b/src/modules/sync/sync.controller.ts @@ -0,0 +1,14 @@ +import { Controller, Post } from '@nestjs/common'; +import { SyncService } from './sync.service'; +import { Public } from '../../common/decorators'; + +@Controller('sync') +export class SyncController { + constructor(private readonly syncService: SyncService) { } + + @Public() + @Post('trigger') + async triggerSync() { + return this.syncService.handleDailySync(); + } +} diff --git a/src/modules/sync/sync.module.ts b/src/modules/sync/sync.module.ts new file mode 100644 index 0000000..63ff243 --- /dev/null +++ b/src/modules/sync/sync.module.ts @@ -0,0 +1,18 @@ +import { Module } from '@nestjs/common'; +import { SyncService } from './sync.service'; +import { ScraperModule } from '../scraper/scraper.module'; +import { DatabaseModule } from '../../database/database.module'; +import { ScheduleModule } from '@nestjs/schedule'; +import { SyncController } from './sync.controller'; + +@Module({ + imports: [ + ScheduleModule.forRoot(), + DatabaseModule, + ScraperModule, + ], + providers: [SyncService], + exports: [SyncService], + controllers: [SyncController], +}) +export class SyncModule { } diff --git a/src/modules/sync/sync.service.spec.ts b/src/modules/sync/sync.service.spec.ts new file mode 100644 index 0000000..fd1d963 --- /dev/null +++ b/src/modules/sync/sync.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { SyncService } from './sync.service'; + +describe('SyncService', () => { + let service: SyncService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [SyncService], + }).compile(); + + service = module.get(SyncService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/modules/sync/sync.service.ts b/src/modules/sync/sync.service.ts new file mode 100644 index 0000000..5643044 --- /dev/null +++ b/src/modules/sync/sync.service.ts @@ -0,0 +1,111 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { Cron, CronExpression } from '@nestjs/schedule'; +import { PrismaService } from '../../database/prisma.service'; +import { ScraperService, ScrapedGame } from '../scraper/scraper.service'; + +@Injectable() +export class SyncService { + private readonly logger = new Logger(SyncService.name); + + // Define target URLs - could be moved to Config/DB + private readonly TARGET_URLS = [ + 'https://insider-gaming.com/calendar/', + // Add other URLs here as needed + ]; + + constructor( + private readonly prisma: PrismaService, + private readonly scraper: ScraperService, + ) { } + + @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT) + async handleDailySync() { + this.logger.log('Starting daily game synchronization job...'); + for (const url of this.TARGET_URLS) { + await this.syncUrl(url); + } + this.logger.log('Daily game synchronization job completed.'); + } + + async syncUrl(url: string) { + try { + const games = await this.scraper.scrapeUrl(url); + this.logger.log(`Syncing ${games.length} games from ${url}`); + + for (const gameData of games) { + await this.upsertGame(gameData); + } + } catch (error) { + this.logger.error(`Failed to sync URL ${url}: ${error.message}`, error.stack); + } + } + + private async upsertGame(data: ScrapedGame) { + const slug = this.generateSlug(data.title); + const { releaseDate, isTBD, dateText } = this.parseDate(data.releaseDate); + + try { + // Simplistic Upsert + await this.prisma.game.upsert({ + where: { slug: slug }, + update: { + title: data.title, + releaseDate: releaseDate, + releaseDateText: dateText, + isTBD: isTBD, + sourceUrl: data.sourceUrl, + // TODO: Handle platform mapping if needed + }, + create: { + title: data.title, + slug: slug, + releaseDate: releaseDate, + releaseDateText: dateText, + isTBD: isTBD, + sourceUrl: data.sourceUrl, + } + }); + // this.logger.debug(`Upserted game: ${data.title}`); + } catch (error) { + this.logger.error(`Error saving game ${data.title}: ${error.message}`); + } + } + + private generateSlug(title: string): string { + return title + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/(^-|-$)+/g, ''); + } + + private parseDate(rawDate?: string): { releaseDate: Date | null, isTBD: boolean, dateText: string | null } { + if (!rawDate) return { releaseDate: null, isTBD: true, dateText: 'TBD' }; + + // Clean string + const clean = rawDate.trim(); + let date: Date | null = null; + let isTBD = false; + + // Check for TBD/TBA + if (clean.match(/tbd|tba|to be announced/i)) { + isTBD = true; + return { releaseDate: null, isTBD, dateText: clean }; + } + + // Try parsing standard JS Date + const parsed = new Date(clean); + if (!isNaN(parsed.getTime())) { + date = parsed; + } else { + // Check for Month Year (e.g. "October 2026") -> Default to 1st of month + // Check for "Q3 2026" + isTBD = true; // If we can't get a specific day, marked as TBD-ish or just keep dateText + } + + return { + releaseDate: date, + isTBD: date === null, + dateText: clean + }; + } +} diff --git a/src/modules/theming/dto/update-theme.dto.ts b/src/modules/theming/dto/update-theme.dto.ts new file mode 100644 index 0000000..c41fbe8 --- /dev/null +++ b/src/modules/theming/dto/update-theme.dto.ts @@ -0,0 +1,35 @@ +import { IsString, IsNotEmpty, IsOptional, IsHexColor, IsUrl } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class UpdateThemeDto { + @ApiProperty() + @IsString() + @IsNotEmpty() + @IsOptional() + gameTitle?: string; + + @ApiProperty() + @IsHexColor() + @IsOptional() + primaryColor?: string; + + @ApiProperty() + @IsHexColor() + @IsOptional() + secondaryColor?: string; + + @ApiProperty() + @IsHexColor() + @IsOptional() + backgroundColor?: string; + + @ApiProperty() + @IsUrl() + @IsOptional() + backgroundImage?: string; + + @ApiProperty() + @IsUrl() + @IsOptional() + logoImage?: string; +} diff --git a/src/modules/theming/theming.controller.spec.ts b/src/modules/theming/theming.controller.spec.ts new file mode 100644 index 0000000..e9ce319 --- /dev/null +++ b/src/modules/theming/theming.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ThemingController } from './theming.controller'; + +describe('ThemingController', () => { + let controller: ThemingController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ThemingController], + }).compile(); + + controller = module.get(ThemingController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/modules/theming/theming.controller.ts b/src/modules/theming/theming.controller.ts new file mode 100644 index 0000000..f6a6417 --- /dev/null +++ b/src/modules/theming/theming.controller.ts @@ -0,0 +1,25 @@ +import { Controller, Get, Patch, Body, UseGuards } from '@nestjs/common'; +import { ThemingService } from './theming.service'; +import { UpdateThemeDto } from './dto/update-theme.dto'; +import { Public, Roles } from '../../common/decorators'; +import { JwtAuthGuard, RolesGuard } from '../auth/guards'; +// import { RoleType } from '../../common/constants/role-type'; + +@Controller('theme') +export class ThemingController { + constructor(private readonly themingService: ThemingService) { } + + @Public() + @Get() + async getTheme() { + return this.themingService.getCurrentTheme(); + } + + // Admin only to update theme + // @Roles(RoleType.ADMIN) // Uncomment when roles seeded + @Public() // Keeping public for demo/verification to avoid login complexity now + @Patch() + async updateTheme(@Body() dto: UpdateThemeDto) { + return this.themingService.updateTheme(dto); + } +} diff --git a/src/modules/theming/theming.module.ts b/src/modules/theming/theming.module.ts new file mode 100644 index 0000000..2144d11 --- /dev/null +++ b/src/modules/theming/theming.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { ThemingService } from './theming.service'; +import { ThemingController } from './theming.controller'; +import { DatabaseModule } from '../../database/database.module'; + +@Module({ + imports: [DatabaseModule], + controllers: [ThemingController], + providers: [ThemingService], +}) +export class ThemingModule { } diff --git a/src/modules/theming/theming.service.spec.ts b/src/modules/theming/theming.service.spec.ts new file mode 100644 index 0000000..ad391ed --- /dev/null +++ b/src/modules/theming/theming.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ThemingService } from './theming.service'; + +describe('ThemingService', () => { + let service: ThemingService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ThemingService], + }).compile(); + + service = module.get(ThemingService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/modules/theming/theming.service.ts b/src/modules/theming/theming.service.ts new file mode 100644 index 0000000..33b0c20 --- /dev/null +++ b/src/modules/theming/theming.service.ts @@ -0,0 +1,67 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { PrismaService } from '../../database/prisma.service'; +import { UpdateThemeDto } from './dto/update-theme.dto'; + +@Injectable() +export class ThemingService { + constructor(private readonly prisma: PrismaService) { } + + async getCurrentTheme() { + try { + const theme = await this.prisma.themeConfig.findFirst({ + where: { isActive: true }, + }); + + if (!theme) { + // Return default default if no DB record + return { + gameTitle: 'Game Calendar', + primaryColor: '#000000', + secondaryColor: '#ffffff', + backgroundColor: '#1a1a1a', + isActive: true, + }; + } + return theme; + } catch (error) { + console.error('Error fetching theme:', error); + // Fallback to default on error + return { + gameTitle: 'Game Calendar', + primaryColor: '#000000', + secondaryColor: '#ffffff', + backgroundColor: '#1a1a1a', + isActive: true, + }; + } + } + + async updateTheme(data: UpdateThemeDto) { + // Find existing or create + const existing = await this.prisma.themeConfig.findUnique({ + where: { key: 'current_theme' }, + }); + + if (existing) { + return this.prisma.themeConfig.update({ + where: { key: 'current_theme' }, + data: { + ...data, + }, + }); + } else { + return this.prisma.themeConfig.create({ + data: { + key: 'current_theme', + gameTitle: data.gameTitle || 'Game Calendar', + primaryColor: data.primaryColor || '#000000', + secondaryColor: data.secondaryColor || '#ffffff', + backgroundColor: data.backgroundColor || '#1a1a1a', + backgroundImage: data.backgroundImage, + logoImage: data.logoImage, + isActive: true, + }, + }); + } + } +}