diff --git a/backend/package-lock.json b/backend/package-lock.json index 907f6c03..59bddbdf 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -24,6 +24,7 @@ "cache-manager-ioredis": "^2.1.0", "class-transformer": "^0.5.1", "class-validator": "^0.15.1", + "helmet": "^7.0.0", "ioredis": "^5.11.1", "passport": "^0.6.0", "passport-jwt": "^4.0.1", @@ -31,9 +32,7 @@ "prom-client": "^15.1.3", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", - feat/refresh-token "socket.io": "^4.8.3", - main "stellar-sdk": "^12.0.2", "typeorm": "^1.0.0", "uuid": "^14.0.1" @@ -47,11 +46,8 @@ "@nestjs/testing": "^11.0.1", "@types/express": "^5.0.0", "@types/jest": "^30.0.0", - feat/refresh-token - "@types/node": "^22.20.0", "@types/multer": "^2.1.0", - "@types/node": "^22.10.7", - main + "@types/node": "^22.20.0", "@types/supertest": "^6.0.2", "ajv": "^8.20.0", "ajv-formats": "^3.0.1", @@ -2227,6 +2223,116 @@ } } }, + "node_modules/@nestjs/cli/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@nestjs/cli/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@nestjs/cli/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@nestjs/cli/node_modules/webpack": { + "version": "5.106.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.106.2.tgz", + "integrity": "sha512-wGN3qcrBQIFmQ/c0AiOAQBvrZ5lmY8vbbMv4Mxfgzqd/B6+9pXtLo73WuS1dSGXM5QYY3hZnIbvx+K1xxe6FyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.20.0", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "loader-runner": "^4.3.1", + "mime-db": "^1.54.0", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.17", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.4" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, "node_modules/@nestjs/common": { "version": "11.1.27", "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.27.tgz", @@ -2314,10 +2420,7 @@ }, "node_modules/@nestjs/jwt": { "version": "11.0.2", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/@nestjs/jwt/-/jwt-11.0.2.tgz", "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.2.tgz", - main "integrity": "sha512-rK8aE/3/Ma45gAWfCksAXUNbOoSOUudU0Kn3rT39htPF7wsYXtKfjALKeKKJbFrIWbLjsbqfXX5bIJNvgBugGA==", "license": "MIT", "dependencies": { @@ -2328,10 +2431,6 @@ "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0" } }, - feat/refresh-token - "node_modules/@nestjs/passport": { - "version": "11.0.5", - "resolved": "https://registry.npmmirror.com/@nestjs/passport/-/passport-11.0.5.tgz", "node_modules/@nestjs/mapped-types": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.1.1.tgz", @@ -2355,7 +2454,6 @@ "node_modules/@nestjs/passport": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-11.0.5.tgz", - main "integrity": "sha512-ulQX6mbjlws92PIM15Naes4F4p2JoxGnIJuUsdXQPT+Oo2sqQmENEZXM7eYuimocfHnKlcfZOuyzbA33LwUlOQ==", "license": "MIT", "peerDependencies": { @@ -2574,9 +2672,9 @@ } }, "node_modules/@nestjs/typeorm": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-11.0.2.tgz", - "integrity": "sha512-KQsmOKqIbfq+I1fe88rDq8QNfTU3C4b8MA2qOImhvIbIDOStcC+m+z2itf7vnWGQK0LZBFjWlYdRBID12qgZeg==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-11.0.3.tgz", + "integrity": "sha512-zJ+E5l7auVVA7c0PsvcMdyvRPKTUqU5s2ToYmOA2QEsXQ42qbUGtK4+1HlRfpHqBkCSXP+phiH4luvf9DyJNog==", "license": "MIT", "peerDependencies": { "@nestjs/common": "^10.0.0 || ^11.0.0", @@ -2713,23 +2811,15 @@ }, "node_modules/@stellar/js-xdr": { "version": "3.1.2", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/@stellar/js-xdr/-/js-xdr-3.1.2.tgz", - "resolved": "https://registry.npmjs.org/@stellar/js-xdr/-/js-xdr-3.1.2.tgz", - main "integrity": "sha512-VVolPL5goVEIsvuGqDc5uiKxV03lzfWdvYg1KikvwheDmTBO68CKDji3bAZ/kppZrx5iTA8z3Ld5yuytcvhvOQ==", "license": "Apache-2.0" }, "node_modules/@stellar/stellar-base": { "version": "12.1.1", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/@stellar/stellar-base/-/stellar-base-12.1.1.tgz", - "integrity": "sha512-gOBSOFDepihslcInlqnxKZdIW9dMUO1tpOm3AtJR33K2OvpXG6SaVHCzAmCFArcCqI9zXTEiSoh70T48TmiHJA==", "resolved": "https://registry.npmjs.org/@stellar/stellar-base/-/stellar-base-12.1.1.tgz", "integrity": "sha512-gOBSOFDepihslcInlqnxKZdIW9dMUO1tpOm3AtJR33K2OvpXG6SaVHCzAmCFArcCqI9zXTEiSoh70T48TmiHJA==", "deprecated": "This package is now rolled into @stellar/stellar-sdk. Please use @stellar/stellar-sdk to continue receiving updates and support.", - main "license": "Apache-2.0", "dependencies": { "@stellar/js-xdr": "^3.1.2", @@ -2745,10 +2835,7 @@ }, "node_modules/@stellar/stellar-base/node_modules/buffer": { "version": "6.0.3", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/buffer/-/buffer-6.0.3.tgz", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - main "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", "funding": [ { @@ -3022,10 +3109,7 @@ }, "node_modules/@types/jsonwebtoken": { "version": "9.0.10", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", - main "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", "license": "MIT", "dependencies": { @@ -3042,11 +3126,6 @@ }, "node_modules/@types/ms": { "version": "2.1.0", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "license": "MIT" - }, "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "license": "MIT" @@ -3061,10 +3140,9 @@ "@types/express": "*" } }, - main "node_modules/@types/node": { "version": "22.20.0", - "resolved": "https://registry.npmmirror.com/@types/node/-/node-22.20.0.tgz", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.0.tgz", "integrity": "sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==", "license": "MIT", "dependencies": { @@ -3145,11 +3223,6 @@ }, "node_modules/@types/validator": { "version": "13.15.10", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/@types/validator/-/validator-13.15.10.tgz", - "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", - "license": "MIT" - }, "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", "license": "MIT" @@ -3163,7 +3236,6 @@ "@types/node": "*" } }, - main "node_modules/@types/yargs": { "version": "17.0.35", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", @@ -4022,10 +4094,7 @@ }, "node_modules/agent-base": { "version": "6.0.2", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - main "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "license": "MIT", "dependencies": { @@ -4071,13 +4140,16 @@ } }, "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, "peerDependencies": { - "ajv": "^6.9.1" + "ajv": "^8.8.2" } }, "node_modules/ansi-colors": { @@ -4209,10 +4281,7 @@ }, "node_modules/available-typed-arrays": { "version": "1.0.7", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - main "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "license": "MIT", "dependencies": { @@ -4227,10 +4296,7 @@ }, "node_modules/axios": { "version": "1.18.1", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/axios/-/axios-1.18.1.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", - main "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", "license": "MIT", "dependencies": { @@ -4399,68 +4465,6 @@ "node": ">=0.12.0" } }, - feat/refresh-token - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/bare-addon-resolve": { - "version": "1.10.0", - "resolved": "https://registry.npmmirror.com/bare-addon-resolve/-/bare-addon-resolve-1.10.0.tgz", - "integrity": "sha512-sSd0jieRJlDaODOzj0oe0RjFVC1QI0ZIjGIdPkbrTXsdVVtENg14c+lHHAhHwmWCZ2nQlMhy8jA3Y5LYPc/isA==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "bare-module-resolve": "^1.10.0", - "bare-semver": "^1.0.0" - }, - "peerDependencies": { - "bare-url": "*" - }, - "peerDependenciesMeta": { - "bare-url": { - "optional": true - } - } - }, - "node_modules/bare-module-resolve": { - "version": "1.12.2", - "resolved": "https://registry.npmmirror.com/bare-module-resolve/-/bare-module-resolve-1.12.2.tgz", - "integrity": "sha512-j+hiD5k99qec4KjJvYsI67q5AOBifmy9JG3oeMVxTmvrhn2sIdp8StrUvZu4YNgwTpO+NhniQG16N1ETDe1k5w==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "bare-semver": "^1.0.0" - }, - "peerDependencies": { - "bare-url": "*" - }, - "peerDependenciesMeta": { - "bare-url": { - "optional": true - } - } - }, - "node_modules/bare-semver": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/bare-semver/-/bare-semver-1.1.0.tgz", - "integrity": "sha512-1Hw5qJ7hXdVt3uPUqjeFTuxyvBUJauvz5A1I2jk8gzjZMHp04n//6nV9MDbG9CMw78JHY2lGV0w6s//LrASm2w==", - "license": "Apache-2.0", - "optional": true - }, - "node_modules/base32.js": { - "version": "0.1.0", - "resolved": "https://registry.npmmirror.com/base32.js/-/base32.js-0.1.0.tgz", - "integrity": "sha512-n3TkB02ixgBOhTvANakDb4xaMXnYUVkNoRFJjQflcqMQhyEKxEHdj3E6N8t8sUQ0mjH/3/JxzlXuz3ul/J90pQ==", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - main "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -4505,10 +4509,7 @@ }, "node_modules/bignumber.js": { "version": "9.3.1", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/bignumber.js/-/bignumber.js-9.3.1.tgz", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", - main "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", "license": "MIT", "engines": { @@ -4665,10 +4666,7 @@ }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - main "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, @@ -4758,10 +4756,7 @@ }, "node_modules/call-bind": { "version": "1.0.9", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.9.tgz", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", - main "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", "license": "MIT", "dependencies": { @@ -4932,19 +4927,13 @@ }, "node_modules/class-transformer": { "version": "0.5.1", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/class-transformer/-/class-transformer-0.5.1.tgz", "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", - main "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", "license": "MIT" }, "node_modules/class-validator": { "version": "0.15.1", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/class-validator/-/class-validator-0.15.1.tgz", "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.15.1.tgz", - main "integrity": "sha512-LqoS80HBBSCVhz/3KloUly0ovokxpdOLR++Al3J3+dHXWt9sTKlKd4eYtoxhxyUjoe5+UcIM+5k9MIxyBWnRTw==", "license": "MIT", "dependencies": { @@ -5352,10 +5341,7 @@ }, "node_modules/define-data-property": { "version": "1.1.4", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.4.tgz", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - main "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "license": "MIT", "dependencies": { @@ -5490,10 +5476,7 @@ }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - main "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", "license": "Apache-2.0", "dependencies": { @@ -5507,9 +5490,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.378", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.378.tgz", - "integrity": "sha512-VinvOAuuPmdD1guEgGv5f2Qp7/vlfqOrUOMYNnOD4wj3pit8kRsQHzfIf6teyUGWo15Tg5+bOJaRunvyltpVWQ==", + "version": "1.5.379", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.379.tgz", + "integrity": "sha512-v/qV5aV5EUA2pGilzUCq5/eyOloZAqDZBu9UMBIzgPpLlprjSR6zswsWBTv0KpqxLGUAZEwhO95ZCt7srymNVA==", "dev": true, "license": "ISC" }, @@ -5979,10 +5962,7 @@ }, "node_modules/eventsource": { "version": "2.0.2", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/eventsource/-/eventsource-2.0.2.tgz", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", - main "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", "license": "MIT", "engines": { @@ -6271,10 +6251,7 @@ }, "node_modules/follow-redirects": { "version": "1.16.0", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.16.0.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", - main "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "funding": [ { @@ -6294,10 +6271,7 @@ }, "node_modules/for-each": { "version": "0.3.5", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/for-each/-/for-each-0.3.5.tgz", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - main "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "license": "MIT", "dependencies": { @@ -6725,10 +6699,7 @@ }, "node_modules/has-property-descriptors": { "version": "1.0.2", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - main "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "license": "MIT", "dependencies": { @@ -6790,6 +6761,15 @@ "node": ">= 0.4" } }, + "node_modules/helmet": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.2.0.tgz", + "integrity": "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/hookified": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", @@ -6826,10 +6806,7 @@ }, "node_modules/https-proxy-agent": { "version": "5.0.1", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - main "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "license": "MIT", "dependencies": { @@ -7001,10 +6978,7 @@ }, "node_modules/is-callable": { "version": "1.2.7", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/is-callable/-/is-callable-1.2.7.tgz", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - main "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "license": "MIT", "engines": { @@ -7088,10 +7062,7 @@ }, "node_modules/is-typed-array": { "version": "1.1.15", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/is-typed-array/-/is-typed-array-1.1.15.tgz", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - main "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "license": "MIT", "dependencies": { @@ -7119,10 +7090,7 @@ }, "node_modules/isarray": { "version": "2.0.5", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/isarray/-/isarray-2.0.5.tgz", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - main "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "license": "MIT" }, @@ -8080,10 +8048,7 @@ }, "node_modules/jsonwebtoken": { "version": "9.0.3", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", - main "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", "license": "MIT", "dependencies": { @@ -8105,10 +8070,7 @@ }, "node_modules/jwa": { "version": "2.0.1", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/jwa/-/jwa-2.0.1.tgz", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", - main "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", "license": "MIT", "dependencies": { @@ -8119,10 +8081,7 @@ }, "node_modules/jws": { "version": "4.0.1", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/jws/-/jws-4.0.1.tgz", "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", - main "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", "license": "MIT", "dependencies": { @@ -8166,10 +8125,7 @@ }, "node_modules/libphonenumber-js": { "version": "1.13.7", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/libphonenumber-js/-/libphonenumber-js-1.13.7.tgz", "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.13.7.tgz", - main "integrity": "sha512-rvr3HIMdOgzhz1RFGjftji+wjoAFlzhqCNqJOU/MKTZQ8d9NZxAR/tI+0weDicyoucqVR0U1GCniqHJ0f8aM2A==", "license": "MIT" }, @@ -8249,10 +8205,7 @@ }, "node_modules/lodash.includes": { "version": "4.3.0", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/lodash.includes/-/lodash.includes-4.3.0.tgz", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - main "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", "license": "MIT" }, @@ -8264,47 +8217,31 @@ }, "node_modules/lodash.isboolean": { "version": "3.0.3", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - main "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", "license": "MIT" }, "node_modules/lodash.isinteger": { "version": "4.0.4", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - main "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", "license": "MIT" }, "node_modules/lodash.isnumber": { "version": "3.0.3", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - main "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", "license": "MIT" }, "node_modules/lodash.isplainobject": { "version": "4.0.6", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - main "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", "license": "MIT" }, "node_modules/lodash.isstring": { "version": "4.0.1", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz", "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - main "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", "license": "MIT" }, @@ -8324,10 +8261,7 @@ }, "node_modules/lodash.once": { "version": "4.1.1", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/lodash.once/-/lodash.once-4.1.1.tgz", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - main "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "license": "MIT" }, @@ -8532,6 +8466,141 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minimizer-webpack-plugin": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/minimizer-webpack-plugin/-/minimizer-webpack-plugin-5.6.1.tgz", + "integrity": "sha512-DoeAZz8Q1C1znwsUzej1fdoi4jCf7/+Em27ouLqfK/+3m8G+D7yDhUwrc3CNhjSzGUN1kn7Iv4sWmjflQHenpw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/minimizer-webpack-plugin/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/minimizer-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/minimizer-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/minimizer-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/minipass": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", @@ -8926,10 +8995,7 @@ }, "node_modules/passport": { "version": "0.6.0", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/passport/-/passport-0.6.0.tgz", "resolved": "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz", - main "integrity": "sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==", "license": "MIT", "dependencies": { @@ -8947,10 +9013,7 @@ }, "node_modules/passport-jwt": { "version": "4.0.1", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/passport-jwt/-/passport-jwt-4.0.1.tgz", "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", - main "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", "license": "MIT", "dependencies": { @@ -8960,10 +9023,7 @@ }, "node_modules/passport-strategy": { "version": "1.0.0", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/passport-strategy/-/passport-strategy-1.0.0.tgz", "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", - main "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", "engines": { "node": ">= 0.4.0" @@ -9048,10 +9108,7 @@ }, "node_modules/pause": { "version": "0.0.1", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/pause/-/pause-0.0.1.tgz", "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", - main "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" }, "node_modules/pg": { @@ -9253,10 +9310,7 @@ }, "node_modules/possible-typed-array-names": { "version": "1.1.0", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - main "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", "license": "MIT", "engines": { @@ -9313,9 +9367,9 @@ } }, "node_modules/prettier": { - "version": "3.8.4", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.4.tgz", - "integrity": "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==", + "version": "3.8.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.5.tgz", + "integrity": "sha512-zxcTTCedNGJM4R8sj/Cq/F0W/c4iE0afWBcBwMTRtw4WHYP9TWkYjdiH3npPRUYsXQCPR0hTU9yjovOu+E6EQA==", "dev": true, "license": "MIT", "bin": { @@ -9398,10 +9452,7 @@ }, "node_modules/proxy-from-env": { "version": "2.1.0", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", - main "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", "license": "MIT", "engines": { @@ -9453,10 +9504,7 @@ }, "node_modules/randombytes": { "version": "2.1.0", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/randombytes/-/randombytes-2.1.0.tgz", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - main "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "license": "MIT", "dependencies": { @@ -9464,12 +9512,16 @@ } }, "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", "license": "MIT", "engines": { "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/raw-body": { @@ -9560,10 +9612,7 @@ }, "node_modules/require-addon": { "version": "1.2.0", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/require-addon/-/require-addon-1.2.0.tgz", "resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.2.0.tgz", -main "integrity": "sha512-VNPDZlYgIYQwWp9jMTzljx+k0ZtatKlcvOhktZ/anNPI3dQ9NXk7cq2U4iJ1wd9IrytRnYhyEocFWbkdPb+MYA==", "license": "Apache-2.0", "optional": true, @@ -9735,6 +9784,16 @@ main "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, "node_modules/schema-utils/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -9801,10 +9860,7 @@ main }, "node_modules/set-function-length": { "version": "1.2.2", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.2.2.tgz", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - main "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "license": "MIT", "dependencies": { @@ -9827,10 +9883,7 @@ main }, "node_modules/sha.js": { "version": "2.4.12", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/sha.js/-/sha.js-2.4.12.tgz", "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", - main "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", "license": "(MIT AND BSD-3-Clause)", "dependencies": { @@ -9966,10 +10019,6 @@ main "node": ">=8" } }, - feat/refresh-token - "node_modules/sodium-native": { - "version": "4.3.3", - "resolved": "https://registry.npmmirror.com/sodium-native/-/sodium-native-4.3.3.tgz", "node_modules/socket.io": { "version": "4.8.3", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", @@ -10057,7 +10106,6 @@ main "node_modules/sodium-native": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-4.3.3.tgz", - main "integrity": "sha512-OnxSlN3uyY8D0EsLHpmm2HOFmKddQVvEMmsakCrXUzSd8kjjbzL413t4ZNF3n0UxSwNgwTyUvkmZHTfuCeiYSw==", "license": "MIT", "optional": true, @@ -10168,10 +10216,7 @@ main }, "node_modules/stellar-sdk": { "version": "12.3.0", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/stellar-sdk/-/stellar-sdk-12.3.0.tgz", "resolved": "https://registry.npmjs.org/stellar-sdk/-/stellar-sdk-12.3.0.tgz", - main "integrity": "sha512-3z7umyuBAHN+vm3zLTKqj7P/bErBFnjrwoanBsNyBHaoek9krUgufNupQSMK67B1p0E2NKD1Z6gYPuZiPfJ2qQ==", "deprecated": "⚠️ This package has moved to @stellar/stellar-sdk! 🚚", "license": "Apache-2.0", @@ -10528,19 +10573,6 @@ main } } }, - "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, "node_modules/terser-webpack-plugin/node_modules/jest-worker": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", @@ -10661,10 +10693,7 @@ main }, "node_modules/to-buffer": { "version": "1.2.2", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/to-buffer/-/to-buffer-1.2.2.tgz", "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", - main "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", "license": "MIT", "dependencies": { @@ -10705,10 +10734,7 @@ main }, "node_modules/toml": { "version": "3.0.0", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/toml/-/toml-3.0.0.tgz", "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", - main "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==", "license": "MIT" }, @@ -10818,7 +10844,7 @@ main }, "node_modules/ts-node": { "version": "10.9.2", - "resolved": "https://registry.npmmirror.com/ts-node/-/ts-node-10.9.2.tgz", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "devOptional": true, "license": "MIT", @@ -10909,10 +10935,7 @@ main }, "node_modules/tweetnacl": { "version": "1.0.3", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/tweetnacl/-/tweetnacl-1.0.3.tgz", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", - main "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", "license": "Unlicense" }, @@ -10985,10 +11008,7 @@ main }, "node_modules/typed-array-buffer": { "version": "1.0.3", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - main "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "license": "MIT", "dependencies": { @@ -11400,10 +11420,7 @@ main }, "node_modules/urijs": { "version": "1.19.11", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/urijs/-/urijs-1.19.11.tgz", "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz", - main "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==", "license": "MIT" }, @@ -11415,10 +11432,7 @@ main }, "node_modules/utils-merge": { "version": "1.0.1", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - main "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", "license": "MIT", "engines": { @@ -11462,10 +11476,7 @@ main }, "node_modules/validator": { "version": "13.15.35", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/validator/-/validator-13.15.35.tgz", "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", - main "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", "license": "MIT", "engines": { @@ -11515,13 +11526,13 @@ main } }, "node_modules/webpack": { - "version": "5.106.2", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.106.2.tgz", - "integrity": "sha512-wGN3qcrBQIFmQ/c0AiOAQBvrZ5lmY8vbbMv4Mxfgzqd/B6+9pXtLo73WuS1dSGXM5QYY3hZnIbvx+K1xxe6FyA==", + "version": "5.108.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.108.1.tgz", + "integrity": "sha512-UUCihHQK3O7483Woa0SulNLDeAiOhHI2PN2PAPU4fVWJqbzhv04EJ8FaWtB9WWh3i8fRt28543U7VfuJTOrpgQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", @@ -11531,20 +11542,19 @@ main "acorn-import-phases": "^1.0.3", "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.20.0", - "es-module-lexer": "^2.0.0", + "enhanced-resolve": "^5.22.2", + "es-module-lexer": "^2.1.0", "eslint-scope": "5.1.1", "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", - "loader-runner": "^4.3.1", + "loader-runner": "^4.3.2", "mime-db": "^1.54.0", + "minimizer-webpack-plugin": "^5.6.1", "neo-async": "^2.6.2", "schema-utils": "^4.3.3", "tapable": "^2.3.0", - "terser-webpack-plugin": "^5.3.17", - "watchpack": "^2.5.1", - "webpack-sources": "^3.3.4" + "watchpack": "^2.5.2", + "webpack-sources": "^3.5.0" }, "bin": { "webpack": "bin/webpack.js" @@ -11582,29 +11592,13 @@ main "node": ">=10.13.0" } }, - "node_modules/webpack/node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, "node_modules/webpack/node_modules/ajv-formats": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -11617,25 +11611,13 @@ main } } }, - "node_modules/webpack/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, "node_modules/webpack/node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -11650,23 +11632,18 @@ main "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=4.0" } }, - "node_modules/webpack/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, "node_modules/webpack/node_modules/schema-utils": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -11699,10 +11676,7 @@ main }, "node_modules/which-typed-array": { "version": "1.1.22", - feat/refresh-token - "resolved": "https://registry.npmmirror.com/which-typed-array/-/which-typed-array-1.1.22.tgz", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", - main "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", "license": "MIT", "dependencies": { diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 06882704..23886186 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -1,4 +1,5 @@ import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common'; +import { APP_GUARD } from '@nestjs/core'; import { DeprecationMiddleware } from './common/versioning/deprecation.middleware'; import { ConfigModule } from '@nestjs/config'; import { TypeOrmModule } from '@nestjs/typeorm'; @@ -9,6 +10,8 @@ import typeormConfig from './config/typeorm.config'; import { RedisModule } from './redis/redis.module'; import { AuthModule } from './auth.module'; import { UsersModule } from './users/users.module'; +import { ThrottlerModule } from './common/throttler/throttler.module'; +import { ThrottlerGuard } from './common/throttler/throttler.guard'; import { CacheModule } from '@nestjs/cache-manager'; import cacheConfig from './config/cache.config'; @@ -30,6 +33,7 @@ import { ChatModule } from './chat/chat.module'; CacheModule.register(cacheConfig), EncryptionModule, RedisModule, + ThrottlerModule, AuthModule, UsersModule, BackupModule, @@ -40,7 +44,11 @@ import { ChatModule } from './chat/chat.module'; ChatModule, ], controllers: [AppController], - providers: [AppService, ShutdownService], + providers: [ + AppService, + ShutdownService, + { provide: APP_GUARD, useClass: ThrottlerGuard }, + ], exports: [ShutdownService], }) export class AppModule implements NestModule { diff --git a/backend/src/auth.controller.spec.ts b/backend/src/auth.controller.spec.ts index b9a88621..1caa5237 100644 --- a/backend/src/auth.controller.spec.ts +++ b/backend/src/auth.controller.spec.ts @@ -3,37 +3,31 @@ import { Keypair } from 'stellar-sdk'; import { AuthController } from './auth.controller'; import { RedisService } from './redis/redis.service'; -// Generate a valid Stellar ED25519 public key at test time const validStellarAddress = Keypair.random().publicKey(); describe('AuthController', () => { let controller: AuthController; - let redisService: RedisService; - let mockClient: Record; + let redisService: jest.Mocked>; beforeEach(() => { - mockClient = { - incr: jest.fn(), - expire: jest.fn(), - }; - redisService = { set: jest.fn(), - getClient: jest.fn().mockReturnValue(mockClient), - } as unknown as RedisService; + get: jest.fn(), + del: jest.fn(), + getClient: jest.fn(), + }; controller = new AuthController( - redisService, - undefined as any, // authService – not exercised in these tests + redisService as unknown as RedisService, + undefined as any, // authService undefined as any, // userService undefined as any, // loginAttemptService undefined as any, // auditLogService + undefined as any, // refreshTokenService ); }); it('returns a nonce and expiresAt for a valid Stellar wallet address', async () => { - mockClient.incr.mockResolvedValue(1); - const result = await controller.getNonce(validStellarAddress); expect(typeof result.nonce).toBe('string'); @@ -46,29 +40,21 @@ describe('AuthController', () => { 300, 'nonce', ); - expect(mockClient.expire).toHaveBeenCalledWith( - `rate:${validStellarAddress}`, - 60, - ); }); it('throws BAD_REQUEST for an invalid Stellar wallet address', async () => { - await expect(controller.getNonce('invalid-address')).rejects.toThrow( - HttpException, - ); - await expect( - controller.getNonce('invalid-address'), - ).rejects.toMatchObject({ status: HttpStatus.BAD_REQUEST }); + await expect(controller.getNonce('invalid-address')).rejects.toMatchObject({ + status: HttpStatus.BAD_REQUEST, + }); }); - it('throws TOO_MANY_REQUESTS when rate limit is exceeded', async () => { - mockClient.incr.mockResolvedValue(6); - - await expect(controller.getNonce(validStellarAddress)).rejects.toThrow( - HttpException, + it('normalizes lowercase wallet address to uppercase', async () => { + const result = await controller.getNonce(validStellarAddress.toLowerCase()); + expect(redisService.set).toHaveBeenCalledWith( + validStellarAddress, + result.nonce, + 300, + 'nonce', ); - await expect( - controller.getNonce(validStellarAddress), - ).rejects.toMatchObject({ status: HttpStatus.TOO_MANY_REQUESTS }); }); }); diff --git a/backend/src/auth.controller.ts b/backend/src/auth.controller.ts index 212d29dd..41cf55c7 100644 --- a/backend/src/auth.controller.ts +++ b/backend/src/auth.controller.ts @@ -9,6 +9,8 @@ import { import { randomBytes } from 'crypto'; import { Keypair } from 'stellar-sdk'; import { normalizeWalletAddress } from './common/utils/wallet.utils'; +import { Throttle } from './common/throttler/throttle.decorator'; +import { ThrottlerGuard } from './common/throttler/throttler.guard'; import { RedisService } from './redis/redis.service'; import { AuthService } from './auth.service'; import { LoginDto } from './auth/login.dto'; @@ -21,6 +23,7 @@ import { RefreshTokenService } from './refresh-token/refresh-token.service'; @ApiTags('auth') @Controller('auth') +@UseGuards(ThrottlerGuard) export class AuthController { constructor( private readonly redisService: RedisService, @@ -33,6 +36,7 @@ export class AuthController { ) {} @Get('nonce/:walletAddress') + @Throttle(5, 60) @ApiOperation({ summary: 'Request a sign-in nonce', description: @@ -68,6 +72,7 @@ export class AuthController { } @Post('login') + @Throttle(10, 60) @ApiOperation({ summary: 'Authenticate with Stellar wallet signature', description: @@ -166,6 +171,7 @@ export class AuthController { } @Post('refresh') + @Throttle(20, 60) async refresh(@Body() body: RefreshTokenDto, @Req() req: any) { const { refreshToken } = body; @@ -239,20 +245,6 @@ export class AuthController { } } - private async enforceRateLimit(walletAddress: string): Promise { - const rateKey = `rate:${walletAddress}`; - const client = this.redisService.getClient(); - const currentCount = await client.incr(rateKey); - - if (currentCount === 1) { - await client.expire(rateKey, 60); - } - - if (currentCount > 5) { - throw new HttpException('Rate limit exceeded', HttpStatus.TOO_MANY_REQUESTS); - } - } - private extractJtiFromToken(token: string): string { try { const parts = token.split('.'); diff --git a/backend/src/common/throttler/throttle.decorator.ts b/backend/src/common/throttler/throttle.decorator.ts new file mode 100644 index 00000000..aacfa931 --- /dev/null +++ b/backend/src/common/throttler/throttle.decorator.ts @@ -0,0 +1,16 @@ +import { SetMetadata } from '@nestjs/common'; + +export const THROTTLE_KEY = 'throttle'; + +export interface ThrottleOptions { + limit: number; + ttl: number; // seconds +} + +/** @Throttle(limit, ttl) — e.g. @Throttle(5, 60) = 5 req/60s */ +export const Throttle = (limit: number, ttl: number): MethodDecorator & ClassDecorator => + SetMetadata(THROTTLE_KEY, { limit, ttl }); + +/** Skip throttling for a route */ +export const SkipThrottle = (): MethodDecorator & ClassDecorator => + SetMetadata(THROTTLE_KEY, null); diff --git a/backend/src/common/throttler/throttler.guard.ts b/backend/src/common/throttler/throttler.guard.ts new file mode 100644 index 00000000..9f4f1885 --- /dev/null +++ b/backend/src/common/throttler/throttler.guard.ts @@ -0,0 +1,79 @@ +import { + CanActivate, + ExecutionContext, + HttpException, + HttpStatus, + Injectable, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import type { Request, Response } from 'express'; +import { THROTTLE_KEY, ThrottleOptions } from './throttle.decorator'; +import { ThrottlerService } from './throttler.service'; + +// Global defaults +const DEFAULT_AUTHENTICATED_LIMIT = 100; +const DEFAULT_UNAUTHENTICATED_LIMIT = 20; +const DEFAULT_TTL = 60; // seconds + +@Injectable() +export class ThrottlerGuard implements CanActivate { + private readonly trustedIps: Set; + + constructor( + private readonly reflector: Reflector, + private readonly throttlerService: ThrottlerService, + ) { + const raw = process.env.THROTTLE_TRUSTED_IPS || ''; + this.trustedIps = new Set(raw.split(',').map((s) => s.trim()).filter(Boolean)); + } + + async canActivate(context: ExecutionContext): Promise { + const meta = this.reflector.getAllAndOverride(THROTTLE_KEY, [ + context.getHandler(), + context.getClass(), + ]); + + // null means @SkipThrottle() + if (meta === null) return true; + + const req = context.switchToHttp().getRequest(); + const res = context.switchToHttp().getResponse(); + + const ip = this.extractIp(req); + + // Bypass for trusted IPs + if (this.trustedIps.has(ip)) return true; + + const userId = req.user?.sub; + const isAuthenticated = Boolean(userId); + + const limit = meta?.limit ?? (isAuthenticated ? DEFAULT_AUTHENTICATED_LIMIT : DEFAULT_UNAUTHENTICATED_LIMIT); + const ttl = meta?.ttl ?? DEFAULT_TTL; + + // Use userId for authenticated requests, IP otherwise + const identifier = userId ? `user:${userId}` : `ip:${ip}`; + // Scope per route to avoid cross-endpoint interference + const routeKey = `${context.getClass().name}:${context.getHandler().name}:${identifier}`; + + const result = await this.throttlerService.check(routeKey, limit, ttl); + + if (!result.allowed) { + res.setHeader('Retry-After', result.retryAfter); + throw new HttpException( + { statusCode: 429, message: 'Too Many Requests', retryAfter: result.retryAfter }, + HttpStatus.TOO_MANY_REQUESTS, + ); + } + + return true; + } + + private extractIp(req: Request): string { + const forwarded = req.headers['x-forwarded-for']; + if (forwarded) { + const first = Array.isArray(forwarded) ? forwarded[0] : forwarded.split(',')[0]; + return first.trim(); + } + return req.socket?.remoteAddress ?? '0.0.0.0'; + } +} diff --git a/backend/src/common/throttler/throttler.module.ts b/backend/src/common/throttler/throttler.module.ts new file mode 100644 index 00000000..ba07e4b3 --- /dev/null +++ b/backend/src/common/throttler/throttler.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { ThrottlerService } from './throttler.service'; +import { ThrottlerGuard } from './throttler.guard'; +import { RedisModule } from '../../redis/redis.module'; + +@Module({ + imports: [RedisModule], + providers: [ThrottlerService, ThrottlerGuard], + exports: [ThrottlerService, ThrottlerGuard], +}) +export class ThrottlerModule {} diff --git a/backend/src/common/throttler/throttler.service.ts b/backend/src/common/throttler/throttler.service.ts new file mode 100644 index 00000000..a494396e --- /dev/null +++ b/backend/src/common/throttler/throttler.service.ts @@ -0,0 +1,49 @@ +import { Injectable } from '@nestjs/common'; +import { RedisService } from '../../redis/redis.service'; + +export interface ThrottleResult { + allowed: boolean; + retryAfter: number; // seconds until oldest request expires +} + +@Injectable() +export class ThrottlerService { + constructor(private readonly redisService: RedisService) {} + + /** + * Sliding window rate limit using Redis sorted sets. + * Key format: throttle: + */ + async check(identifier: string, limit: number, ttl: number): Promise { + const key = `throttle:${identifier}`; + const now = Date.now(); + const windowStart = now - ttl * 1000; + const client = this.redisService.getClient(); + + // Atomic sliding window: remove expired entries, count, conditionally add + const [, count] = await client + .multi() + .zremrangebyscore(key, '-inf', windowStart) + .zcard(key) + .exec() as [any, [null, number]]; + + const currentCount = count[1]; + + if (currentCount >= limit) { + // Get the oldest entry's score to compute retry-after + const oldest = await client.zrange(key, 0, 0, 'WITHSCORES'); + const oldestTs = oldest.length >= 2 ? parseInt(oldest[1], 10) : now; + const retryAfter = Math.ceil((oldestTs + ttl * 1000 - now) / 1000); + return { allowed: false, retryAfter: Math.max(1, retryAfter) }; + } + + // Add current request with timestamp as score + await client + .multi() + .zadd(key, now, `${now}-${Math.random()}`) + .expire(key, ttl) + .exec(); + + return { allowed: true, retryAfter: 0 }; + } +} diff --git a/backend/src/common/throttler/throttler.spec.ts b/backend/src/common/throttler/throttler.spec.ts new file mode 100644 index 00000000..2831d147 --- /dev/null +++ b/backend/src/common/throttler/throttler.spec.ts @@ -0,0 +1,176 @@ +import { ThrottlerService } from './throttler.service'; + +const mockMulti = { + zremrangebyscore: jest.fn().mockReturnThis(), + zcard: jest.fn().mockReturnThis(), + zadd: jest.fn().mockReturnThis(), + expire: jest.fn().mockReturnThis(), + exec: jest.fn(), +}; + +const mockClient = { + multi: jest.fn(() => mockMulti), + zrange: jest.fn(), +}; + +const mockRedisService = { getClient: jest.fn(() => mockClient) }; + +describe('ThrottlerService', () => { + let service: ThrottlerService; + + beforeEach(() => { + jest.clearAllMocks(); + service = new ThrottlerService(mockRedisService as any); + }); + + it('allows request when under limit', async () => { + // zremrangebyscore returns 0, zcard returns 2 (below limit of 5) + mockMulti.exec.mockResolvedValueOnce([[null, 0], [null, 2]]); + mockMulti.exec.mockResolvedValueOnce([[null, 'OK'], [null, 1]]); + + const result = await service.check('test-key', 5, 60); + + expect(result.allowed).toBe(true); + expect(result.retryAfter).toBe(0); + }); + + it('blocks request when at limit', async () => { + mockMulti.exec.mockResolvedValueOnce([[null, 0], [null, 5]]); + mockClient.zrange.mockResolvedValue([`${Date.now() - 30000}`, `${Date.now() - 30000}`]); + + const result = await service.check('test-key', 5, 60); + + expect(result.allowed).toBe(false); + expect(result.retryAfter).toBeGreaterThan(0); + }); + + it('returns retryAfter >= 1 when blocked', async () => { + const oldTs = Date.now() - 59_000; // 59s ago, so ~1s remaining + mockMulti.exec.mockResolvedValueOnce([[null, 0], [null, 10]]); + mockClient.zrange.mockResolvedValue([String(oldTs), String(oldTs)]); + + const result = await service.check('test-key', 10, 60); + + expect(result.allowed).toBe(false); + expect(result.retryAfter).toBeGreaterThanOrEqual(1); + }); +}); + +describe('ThrottlerGuard', () => { + const { ThrottlerGuard } = require('./throttler.guard'); + const { Reflector } = require('@nestjs/core'); + + const mockReflector = { getAllAndOverride: jest.fn() }; + const mockThrottlerService = { check: jest.fn() }; + + let guard: typeof ThrottlerGuard.prototype; + + const makeContext = (overrides: { + user?: any; + ip?: string; + headers?: Record; + setHeader?: jest.Mock; + } = {}) => { + const res = { setHeader: overrides.setHeader ?? jest.fn() }; + const req = { + user: overrides.user, + socket: { remoteAddress: overrides.ip ?? '127.0.0.1' }, + headers: overrides.headers ?? {}, + }; + return { + switchToHttp: () => ({ + getRequest: () => req, + getResponse: () => res, + }), + getHandler: () => ({ name: 'testHandler' }), + getClass: () => ({ name: 'TestController' }), + }; + }; + + beforeEach(() => { + jest.clearAllMocks(); + // Default: no THROTTLE_TRUSTED_IPS env + delete process.env.THROTTLE_TRUSTED_IPS; + guard = new ThrottlerGuard(mockReflector as any, mockThrottlerService as any); + }); + + it('skips throttling when meta is null (@SkipThrottle)', async () => { + mockReflector.getAllAndOverride.mockReturnValue(null); + const result = await guard.canActivate(makeContext()); + expect(result).toBe(true); + expect(mockThrottlerService.check).not.toHaveBeenCalled(); + }); + + it('allows request when under limit', async () => { + mockReflector.getAllAndOverride.mockReturnValue({ limit: 10, ttl: 60 }); + mockThrottlerService.check.mockResolvedValue({ allowed: true, retryAfter: 0 }); + const result = await guard.canActivate(makeContext()); + expect(result).toBe(true); + }); + + it('throws 429 when over limit and sets Retry-After header', async () => { + mockReflector.getAllAndOverride.mockReturnValue({ limit: 5, ttl: 60 }); + mockThrottlerService.check.mockResolvedValue({ allowed: false, retryAfter: 42 }); + const setHeader = jest.fn(); + await expect(guard.canActivate(makeContext({ setHeader }))).rejects.toMatchObject({ + response: { statusCode: 429 }, + }); + expect(setHeader).toHaveBeenCalledWith('Retry-After', 42); + }); + + it('uses userId as identifier for authenticated requests', async () => { + mockReflector.getAllAndOverride.mockReturnValue({ limit: 100, ttl: 60 }); + mockThrottlerService.check.mockResolvedValue({ allowed: true, retryAfter: 0 }); + await guard.canActivate(makeContext({ user: { sub: 'user-123' } })); + expect(mockThrottlerService.check).toHaveBeenCalledWith( + expect.stringContaining('user:user-123'), + 100, + 60, + ); + }); + + it('uses IP as identifier for unauthenticated requests', async () => { + mockReflector.getAllAndOverride.mockReturnValue({ limit: 20, ttl: 60 }); + mockThrottlerService.check.mockResolvedValue({ allowed: true, retryAfter: 0 }); + await guard.canActivate(makeContext({ ip: '192.168.1.1' })); + expect(mockThrottlerService.check).toHaveBeenCalledWith( + expect.stringContaining('ip:192.168.1.1'), + 20, + 60, + ); + }); + + it('bypasses throttle for trusted IPs', async () => { + process.env.THROTTLE_TRUSTED_IPS = '10.0.0.1,10.0.0.2'; + guard = new ThrottlerGuard(mockReflector as any, mockThrottlerService as any); + mockReflector.getAllAndOverride.mockReturnValue({ limit: 5, ttl: 60 }); + const result = await guard.canActivate(makeContext({ ip: '10.0.0.1' })); + expect(result).toBe(true); + expect(mockThrottlerService.check).not.toHaveBeenCalled(); + }); + + it('respects X-Forwarded-For header', async () => { + mockReflector.getAllAndOverride.mockReturnValue({ limit: 20, ttl: 60 }); + mockThrottlerService.check.mockResolvedValue({ allowed: true, retryAfter: 0 }); + await guard.canActivate(makeContext({ headers: { 'x-forwarded-for': '203.0.113.5, 10.0.0.1' } })); + expect(mockThrottlerService.check).toHaveBeenCalledWith( + expect.stringContaining('ip:203.0.113.5'), + 20, + 60, + ); + }); + + it('uses default unauthenticated limit when no meta decorator', async () => { + mockReflector.getAllAndOverride.mockReturnValue(undefined); + mockThrottlerService.check.mockResolvedValue({ allowed: true, retryAfter: 0 }); + await guard.canActivate(makeContext()); + expect(mockThrottlerService.check).toHaveBeenCalledWith(expect.any(String), 20, 60); + }); + + it('uses default authenticated limit when no meta decorator', async () => { + mockReflector.getAllAndOverride.mockReturnValue(undefined); + mockThrottlerService.check.mockResolvedValue({ allowed: true, retryAfter: 0 }); + await guard.canActivate(makeContext({ user: { sub: 'user-abc' } })); + expect(mockThrottlerService.check).toHaveBeenCalledWith(expect.any(String), 100, 60); + }); +});